Add full webactivity support (bug 936842)
This commit is contained in:
Родитель
d76e5c3c39
Коммит
e80a91ba22
|
@ -129,7 +129,13 @@ class Spec(object):
|
|||
|
||||
# Check that the node is of the proper type. If it isn't, then we need
|
||||
# to stop iterating at this point.
|
||||
if not isinstance(branch, spec_branch["expected_type"]):
|
||||
exp_type = spec_branch.get("expected_type")
|
||||
if (exp_type and
|
||||
not isinstance(branch, exp_type) or
|
||||
# Handle `isinstance(True, int) == True` :(
|
||||
(isinstance(branch, bool) and
|
||||
(exp_type == int if isinstance(exp_type, type) else
|
||||
bool not in exp_type))):
|
||||
self.error(
|
||||
err_id=("spec", "iterate", "bad_type"),
|
||||
error="%s's `%s` was of an unexpected type." %
|
||||
|
@ -163,21 +169,21 @@ class Spec(object):
|
|||
error="`%s` contains an invalid value in %s" %
|
||||
(branch_name, self.SPEC_NAME),
|
||||
description=["A `%s` was encountered while validating a "
|
||||
"%s containing the value '%s'. This value is "
|
||||
"not appropriate for this type of element." %
|
||||
"`%s` containing the value '%s'. This value "
|
||||
"is not appropriate for this type of "
|
||||
"element." %
|
||||
(branch_name, self.SPEC_NAME, branch),
|
||||
self.MORE_INFO])
|
||||
elif ("value_matches" in spec_branch and
|
||||
isinstance(branch, types.StringTypes)):
|
||||
raw_pattern = spec_branch.get("value_matches")
|
||||
pattern = re.compile(raw_pattern)
|
||||
if not pattern.match(branch):
|
||||
raw_pattern = spec_branch["value_matches"]
|
||||
if not re.match(raw_pattern, branch):
|
||||
self.error(
|
||||
err_id=("spec", "iterate", "value_pattern_fail"),
|
||||
error="`%s` contains an invalid value in %s" %
|
||||
(branch_name, self.SPEC_NAME),
|
||||
description=["A `%s` was encountered while validating "
|
||||
"a %s. Its value does not match the "
|
||||
"a `%s`. Its value does not match the "
|
||||
"pattern required for `%s`s." %
|
||||
(branch_name, self.SPEC_NAME,
|
||||
branch_name),
|
||||
|
@ -197,7 +203,7 @@ class Spec(object):
|
|||
self.MORE_INFO])
|
||||
|
||||
# The rest of the tests are for child items.
|
||||
if not isinstance(branch, list):
|
||||
if not isinstance(branch, (list, tuple)):
|
||||
return
|
||||
|
||||
if "child_nodes" in spec_branch:
|
||||
|
|
|
@ -8,6 +8,9 @@ from ..constants import DESCRIPTION_TYPES
|
|||
from ..specprocessor import Spec, LITERAL_TYPE
|
||||
|
||||
|
||||
# This notably excludes booleans.
|
||||
JSON_LITERALS = types.StringTypes + (int, float)
|
||||
|
||||
BANNED_ORIGINS = [
|
||||
"gaiamobile.org",
|
||||
"mozilla.com",
|
||||
|
@ -20,6 +23,50 @@ _FULL_PERMISSIONS = ("readonly", "readwrite", "readcreate", "createonly")
|
|||
|
||||
FXOS_ICON_SIZES = (60, 90, 120)
|
||||
|
||||
FILTER_DEF_OBJ = {
|
||||
"expected_type": dict,
|
||||
"not_empty": True,
|
||||
"child_nodes": {
|
||||
"required": {"expected_type": bool},
|
||||
"value": {
|
||||
"expected_type": JSON_LITERALS + (list, tuple),
|
||||
"not_empty": True,
|
||||
"child_nodes": {"expected_type": JSON_LITERALS},
|
||||
},
|
||||
"min": {"expected_type": (int, float)},
|
||||
"max": {"expected_type": (int, float)},
|
||||
"pattern": {"expected_type": types.StringTypes},
|
||||
"regexp": {"expected_type": types.StringTypes}, # FXOS 1.0/1.1
|
||||
"patternFlags": {
|
||||
"expected_type": types.StringTypes,
|
||||
"max_length": 4,
|
||||
"value_matches": "[igmy]+",
|
||||
},
|
||||
},
|
||||
}
|
||||
FILTER_DEF_OBJ["allowed_once_nodes"] = FILTER_DEF_OBJ["child_nodes"].keys()
|
||||
|
||||
WEB_ACTIVITY_HANDLER = {
|
||||
"href": {"expected_type": types.StringTypes,
|
||||
"process": lambda s: s.process_act_href,
|
||||
"not_empty": True},
|
||||
"disposition": {"expected_type": types.StringTypes,
|
||||
"values": ["window", "inline"]},
|
||||
"filters": {
|
||||
"expected_type": dict,
|
||||
"allowed_nodes": ["*"],
|
||||
"not_empty": True,
|
||||
"child_nodes": {
|
||||
"*": {
|
||||
"expected_type": JSON_LITERALS + (list, dict),
|
||||
"not_empty": True,
|
||||
"process": lambda s: s.process_act_filter,
|
||||
}
|
||||
},
|
||||
},
|
||||
"returnValue": {"expected_type": bool},
|
||||
}
|
||||
|
||||
|
||||
class WebappSpec(Spec):
|
||||
"""This object parses and subsequently validates webapp manifest files."""
|
||||
|
@ -129,26 +176,10 @@ class WebappSpec(Spec):
|
|||
"*": {
|
||||
"expected_type": dict,
|
||||
"required_nodes": ["href"],
|
||||
"allowed_once_nodes": ["disposition", "filters"],
|
||||
"child_nodes": {
|
||||
"href": {"expected_type": types.StringTypes,
|
||||
"process": lambda s: s.process_act_href,
|
||||
"not_empty": True},
|
||||
"disposition": {"expected_type": types.StringTypes,
|
||||
"values": ["window", "inline"]},
|
||||
"filters": {
|
||||
"expected_type": dict,
|
||||
"allowed_nodes": ["*"],
|
||||
"child_nodes":
|
||||
{"*": {"expected_type": DESCRIPTION_TYPES,
|
||||
"process":
|
||||
lambda s: s.process_act_type,
|
||||
"not_empty": True},
|
||||
"number": {"expected_type": LITERAL_TYPE}}
|
||||
},
|
||||
"returnValue": {
|
||||
"expected_type": bool}
|
||||
}
|
||||
"allowed_once_nodes": [
|
||||
"disposition", "filters", "returnValue"
|
||||
],
|
||||
"child_nodes": WEB_ACTIVITY_HANDLER,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -504,16 +535,38 @@ class WebappSpec(Spec):
|
|||
"Found: %s" % node,
|
||||
self.MORE_INFO])
|
||||
|
||||
def process_act_type(self, node):
|
||||
if (isinstance(node, list) and
|
||||
not all(isinstance(s, types.StringTypes) for s in node)):
|
||||
def process_act_filter(self, node):
|
||||
if isinstance(node, JSON_LITERALS):
|
||||
# Standard JSON literals can be safely ignored.
|
||||
return
|
||||
|
||||
if isinstance(node, list):
|
||||
# Arrays must contain only JSON literals.
|
||||
if not all(isinstance(s, JSON_LITERALS) and
|
||||
not isinstance(s, bool) for s in node):
|
||||
self.error(
|
||||
err_id=("spec", "webapp", "act_type"),
|
||||
error="Activity filter is not valid.",
|
||||
description=[
|
||||
"The value for an activity's filter must either "
|
||||
"be a basic value or array of basic values.",
|
||||
"Found: [%s]" % ", ".join(map(repr, node)),
|
||||
self.MORE_INFO])
|
||||
|
||||
elif isinstance(node, dict):
|
||||
# Objects are filter definition objects, which have rules.
|
||||
return self._iterate(self.path[-1], node, FILTER_DEF_OBJ)
|
||||
|
||||
else:
|
||||
# Everything else is invalid.
|
||||
self.error(
|
||||
err_id=("spec", "webapp", "act_type"),
|
||||
error="Activity `type` is not valid.",
|
||||
description=["The `type` value for an activity must either be "
|
||||
"a string or array of strings.",
|
||||
"Found: [%s]" % ", ".join(map(str, node)),
|
||||
self.MORE_INFO])
|
||||
error="Activity filter is not valid.",
|
||||
description=[
|
||||
"The value for an activity's filter must either "
|
||||
"be a basic value or array of basic values.",
|
||||
"Found: %s" % repr(node),
|
||||
self.MORE_INFO])
|
||||
|
||||
def process_orientation(self, node):
|
||||
values = [u"portrait", u"landscape", u"portrait-secondary",
|
||||
|
|
|
@ -42,10 +42,10 @@ class TestWebappAccessories(TestCase):
|
|||
eq_(s._path_valid("data:asdf", can_be_data=True), True)
|
||||
|
||||
|
||||
class TestWebapps(TestCase):
|
||||
class WebappBaseTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestWebapps, self).setUp()
|
||||
super(WebappBaseTestCase, self).setUp()
|
||||
self.listed = False
|
||||
|
||||
descr = "Exciting Open Web development action!"
|
||||
|
@ -126,6 +126,8 @@ class TestWebapps(TestCase):
|
|||
appvalidator.webapp.detect_webapp(self.err, name)
|
||||
os.unlink(name)
|
||||
|
||||
|
||||
class TestWebapps(WebappBaseTestCase):
|
||||
def test_pass(self):
|
||||
"""Test that a bland webapp file throws no errors."""
|
||||
self.analyze()
|
||||
|
@ -610,82 +612,9 @@ class TestWebapps(TestCase):
|
|||
self.analyze()
|
||||
self.assert_silent()
|
||||
|
||||
def test_act_base(self):
|
||||
"""Test that the most basic web activity passes."""
|
||||
|
||||
self.data["activities"] = {
|
||||
"foo": {"href": "/foo/bar"}
|
||||
}
|
||||
self.analyze()
|
||||
self.assert_silent()
|
||||
|
||||
def test_act_full(self):
|
||||
"""Test that the fullest web activity passes."""
|
||||
|
||||
self.data["activities"] = {
|
||||
"foo": {"href": "/foo/bar",
|
||||
"disposition": "window",
|
||||
"filters": {"type": "foo", "number": 1}},
|
||||
"bar": {"href": "foo/bar",
|
||||
"disposition": "inline",
|
||||
"filters": {"whatever": ["foo", "bar"]}}
|
||||
}
|
||||
self.analyze()
|
||||
self.assert_silent()
|
||||
|
||||
def test_act_bad_href(self):
|
||||
"""Test that bad activity hrefs are disallowed."""
|
||||
|
||||
self.data["activities"] = {
|
||||
"foo": {"href": "http://foo.bar/asdf",
|
||||
"disposition": "window",
|
||||
"filters": {"type": "foo"}}
|
||||
}
|
||||
self.analyze()
|
||||
self.assert_failed(with_errors=True)
|
||||
|
||||
def test_act_bad_disp(self):
|
||||
"""Test that the disposition of an activity is correct."""
|
||||
|
||||
self.data["activities"] = {
|
||||
"foo": {"href": "/foo/bar",
|
||||
"disposition": "lol not a disposition",
|
||||
"filters": {"type": "foo"}}
|
||||
}
|
||||
self.analyze()
|
||||
self.assert_failed(with_errors=True)
|
||||
|
||||
def test_act_bad_filter_type(self):
|
||||
"""Test that the filter values are correct."""
|
||||
|
||||
self.data["activities"] = {
|
||||
"foo": {"href": "/foo/bar",
|
||||
"disposition": "window",
|
||||
"filters": {"type": 2}}
|
||||
}
|
||||
self.analyze()
|
||||
self.assert_failed(with_errors=True)
|
||||
|
||||
def test_act_bad_filter_base_type(self):
|
||||
"""Test that the filter values are correct."""
|
||||
|
||||
self.data["activities"] = {
|
||||
"foo": {"href": "/foo/bar",
|
||||
"disposition": "window",
|
||||
"filters": "this is not a dict"}
|
||||
}
|
||||
self.analyze()
|
||||
self.assert_failed(with_errors=True)
|
||||
|
||||
def test_act_missing_href(self):
|
||||
"""Test that activities require an href."""
|
||||
|
||||
self.data["activities"] = {
|
||||
"foo": {"disposition": "window",
|
||||
"filters": {"type": "foo"}}
|
||||
}
|
||||
self.analyze()
|
||||
self.assert_failed(with_errors=True)
|
||||
###########
|
||||
# Web activities are tested in tests/test_webapp_activity.py
|
||||
###########
|
||||
|
||||
def test_act_root_type(self):
|
||||
"""Test that the most basic web activity passes."""
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
from test_webapp import WebappBaseTestCase
|
||||
|
||||
|
||||
class TestWebappActivity(WebappBaseTestCase):
|
||||
"""
|
||||
This suite tests that web activities are properly handled for all
|
||||
reasonable combinations of valid nodes.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestWebappActivity, self).setUp()
|
||||
self.ad = self.data["activities"] = {
|
||||
"simple": {
|
||||
"href": "url.html",
|
||||
"disposition": "window",
|
||||
"returnValue": True,
|
||||
"filters": {
|
||||
"literal": 123,
|
||||
"array": ["literal", 123],
|
||||
"filter_obj": {
|
||||
"required": True,
|
||||
"value": "literal",
|
||||
"min": 1,
|
||||
"max": 2,
|
||||
"pattern": "asdf",
|
||||
"patternFlags": "ig",
|
||||
"regexp": "asdf",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
self.simple = self.ad["simple"]
|
||||
|
||||
def broken(self):
|
||||
self.analyze()
|
||||
return self.assert_failed(with_errors=True)
|
||||
|
||||
def suspicious(self):
|
||||
self.analyze()
|
||||
return self.assert_failed(with_warnings=True)
|
||||
|
||||
def test_pass(self):
|
||||
self.analyze()
|
||||
self.assert_silent()
|
||||
|
||||
def test_missing_href(self):
|
||||
del self.simple["href"]
|
||||
self.broken()
|
||||
|
||||
def test_min_features(self):
|
||||
self.data["activities"] = {
|
||||
"simple": {"href": "foo.html"},
|
||||
}
|
||||
self.analyze()
|
||||
self.assert_silent()
|
||||
|
||||
def test_bad_href(self):
|
||||
self.simple["href"] = "http://foo.bar/asdf"
|
||||
self.broken()
|
||||
|
||||
def test_bad_disposition(self):
|
||||
self.simple["disposition"] = "not a disposition"
|
||||
self.broken()
|
||||
|
||||
def test_bad_returnValue(self):
|
||||
self.simple["returnValue"] = "foo"
|
||||
self.broken()
|
||||
|
||||
def test_bad_extra(self):
|
||||
self.simple["extra"] = "this isn't part of the spec!"
|
||||
self.suspicious()
|
||||
|
||||
def test_bad_filter_base(self):
|
||||
self.simple["filters"] = "foo"
|
||||
self.broken()
|
||||
|
||||
def test_empty_filters(self):
|
||||
self.simple["filters"] = {}
|
||||
self.broken()
|
||||
|
||||
def test_bad_basic_values(self):
|
||||
# Basic values can't be boolean, according to the spec.
|
||||
self.simple["filters"]["literal"] = True
|
||||
self.broken()
|
||||
|
||||
def test_bad_basic_values_in_array(self):
|
||||
self.simple["filters"]["array"].append(True)
|
||||
self.broken()
|
||||
|
||||
def test_empty_filterobj(self):
|
||||
self.simple["filters"]["filter_obj"] = {}
|
||||
self.broken()
|
||||
|
||||
def test_extra_filterobj(self):
|
||||
self.simple["filters"]["filter_obj"]["extra"] = "foo"
|
||||
self.suspicious()
|
||||
|
||||
def test_bad_filterobj_required(self):
|
||||
self.simple["filters"]["filter_obj"]["required"] = "foo"
|
||||
self.broken()
|
||||
|
||||
def test_bad_filterobj_value(self):
|
||||
self.simple["filters"]["filter_obj"]["value"] = True
|
||||
self.broken()
|
||||
|
||||
def test_bad_filterobj_value_array(self):
|
||||
self.simple["filters"]["filter_obj"]["value"] = [True]
|
||||
self.broken()
|
||||
|
||||
def test_filterobj_value_array(self):
|
||||
self.simple["filters"]["filter_obj"]["value"] = [123, "foo"]
|
||||
self.analyze()
|
||||
self.assert_silent()
|
||||
|
||||
def test_bad_filterobj_pattern(self):
|
||||
self.simple["filters"]["filter_obj"]["pattern"] = 123
|
||||
self.broken()
|
||||
|
||||
def test_bad_filterobj_patternFlags(self):
|
||||
self.simple["filters"]["filter_obj"]["patternFlags"] = "asdf"
|
||||
self.broken()
|
||||
|
||||
def test_bad_filterobj_patternFlags_length(self):
|
||||
self.simple["filters"]["filter_obj"]["patternFlags"] = "iiiii"
|
||||
self.broken()
|
||||
|
||||
def test_bad_filterobj_regexp(self):
|
||||
self.simple["filters"]["filter_obj"]["regexp"] = 123
|
||||
self.broken()
|
||||
|
||||
def test_filterobj_optional_elements(self):
|
||||
self.simple["filters"]["filter_obj"] = {"min": 1}
|
||||
self.analyze()
|
||||
self.assert_silent()
|
||||
self.simple["filters"]["filter_obj"] = {"required": True}
|
||||
self.analyze()
|
||||
self.assert_silent()
|
||||
# Thus, no one field is required.
|
||||
|
Загрузка…
Ссылка в новой задаче