# -*- coding: utf-8 -*-
import os
import tempfile
import types

import json
from mock import patch
from nose.tools import eq_

from helper import TestCase
import appvalidator.constants
from appvalidator.errorbundle import ErrorBundle
from appvalidator.specs.webapps import WebappSpec
import appvalidator.webapp


class TestWebappAccessories(TestCase):
    """
    Test that helper functions for webapp manifests work as they are intended
    to.
    """

    def test_path(self):
        """Test that paths are tested properly for allowances."""

        s = WebappSpec("{}", ErrorBundle())

        eq_(s._path_valid("*"), False)
        eq_(s._path_valid("*", can_be_asterisk=True), True)
        eq_(s._path_valid("/foo/bar"), False)
        eq_(s._path_valid("/foo/bar", can_be_absolute=True), True)
        eq_(s._path_valid("//foo/bar"), False)
        eq_(s._path_valid("//foo/bar", can_be_absolute=True), False)
        eq_(s._path_valid("//foo/bar", can_be_relative=True), False)
        eq_(s._path_valid("http://asdf/"), False)
        eq_(s._path_valid("https://asdf/"), False)
        eq_(s._path_valid("ftp://asdf/"), False)
        eq_(s._path_valid("http://asdf/", can_have_protocol=True), True)
        eq_(s._path_valid("https://asdf/", can_have_protocol=True), True)
        # No FTP for you!
        eq_(s._path_valid("ftp://asdf/", can_have_protocol=True), False)
        eq_(s._path_valid("data:asdf"), False)
        eq_(s._path_valid("data:asdf", can_be_data=True), True)


class WebappBaseTestCase(TestCase):

    def setUp(self):
        super(WebappBaseTestCase, self).setUp()
        self.listed = False

        descr = "Exciting Open Web development action!"
        descr += (1024 - len(descr)) * "_"

        self.data = {
            "version": "1.0",
            "name": "MozBall",
            "description": descr,
            "icons": {
                "32": "/img/icon-32.png",
                "128": "/img/icon-128.png",
            },
            "developer": {
                "name": "Mozilla Labs",
                "url": "http://mozillalabs.com"
            },
            "installs_allowed_from": [
                "https://appstore.mozillalabs.com",
                "HTTP://mozilla.com/AppStore"
            ],
            "launch_path": "/index.html",
            "locales": {
                "es": {
                    "name": "Foo Bar",
                    "description": "¡Acción abierta emocionante del desarrollo",
                    "developer": {
                        "url": "http://es.mozillalabs.com/"
                    }
                },
                "it": {
                    "description": "Azione aperta emozionante di sviluppo di!",
                    "developer": {
                        "url": "http://it.mozillalabs.com/"
                    }
                }
            },
            "default_locale": "en",
            "screen_size": {
                "min_width": "600",
                "min_height": "300"
            },
            "required_features": [
                "touch", "geolocation", "webgl"
            ],
            "orientation": "landscape",
            "fullscreen": "true",
            "type": "web",
            "precompile": [
                "game.js",
                "database.js"
            ],
        }

        self.resources = [("app_type", "web")]

    def make_privileged(self):
        self.resources = [("app_type", "privileged"),
                          ("packaged", True)]
        self.data["type"] = "privileged"

    def analyze(self):
        """Run the webapp tests on the file."""
        self.detected_type = appvalidator.constants.PACKAGE_WEBAPP
        self.setup_err()

        for resource, value in self.resources:
            self.err.save_resource(resource, value)

        with tempfile.NamedTemporaryFile(delete=False) as t:
            if isinstance(self.data, types.StringTypes):
                t.write(self.data)
            else:
                t.write(json.dumps(self.data))
            name = t.name
        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()
        self.assert_silent()

        output = json.loads(self.err.render_json())
        assert "manifest" in output and output["manifest"]

    def test_bom(self):
        """Test that a plain webapp with a BOM won't throw errors."""
        self.setup_err()
        appvalidator.webapp.detect_webapp(
            self.err, "tests/resources/unicodehelper/utf8_webapp.json")
        self.assert_silent()

    def test_fail_parse(self):
        """Test that invalid JSON is reported."""
        self.data = "}{"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_missing_required(self):
        """Test that missing the name element is a bad thing."""
        del self.data["name"]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_invalid_name(self):
        """Test that the name element is a string."""
        self.data["name"] = ["foo", "bar"]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_long_name(self):
        """Test that long names are flagged for truncation in Gaia."""
        self.data["name"] = "This is a long name."
        self.analyze()
        self.assert_failed(with_warnings=True)

    def test_long_name_with_locale(self):
        """Test that long localized names are flagged for truncation in
        Gaia."""
        self.data["locales"]["es"]["name"] = "This is a long name."
        self.analyze()
        self.assert_failed(with_warnings=True)

    def test_role(self):
        """Test that app may contain role element."""
        self.data["role"] = "input"
        self.analyze()
        self.assert_silent()

    def test_langpack_role_need_languages_target(self):
        """Test that a language-target is needed for langpacks."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://blah.gaiamobile.org/manifest.webapp": "/de/blah",
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    }
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "missing_req_cond", ))

    def test_langpack_languages_target_version_value(self):
        """Test that language-target version is in major.minor format."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.5"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://blah.gaiamobile.org/manifest.webapp": "/de/blah",
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    }
                }
            },
        })
        self.analyze()
        self.assert_silent()

    def test_langpack_languages_target_version_value_suffix(self):
        """Test that language-target version can have a dash suffix."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.5-xyz"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://blah.gaiamobile.org/manifest.webapp": "/de/blah",
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    }
                }
            },
        })
        self.analyze()
        self.assert_silent()

    def test_langpack_invalid_languages_target_type(self):
        """Test language-target type."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://blah.gaiamobile.org/manifest.webapp": "/de/blah",
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    }
                }
            },
            "languages-target": ["2.2"],  # Wrong type.
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "bad_type", ))

    def test_langpack_invalid_languages_target_wrong_version_type(self):
        """Test that language-target version number has the correct type."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": 2.2  # Wrong type.
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://blah.gaiamobile.org/manifest.webapp": "/de/blah",
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    }
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "bad_type", ))

    def test_langpack_invalid_languages_target_wrong_version_value(self):
        """Test that language-target version number has the correct value."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2"  # Wrong value.
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://blah.gaiamobile.org/manifest.webapp": "/de/blah",
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    }
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "value_pattern_fail", ))

    def test_langpack_invalid_languages_target(self):
        """Test that language-target manifest has the correct value."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://my.manifest.webapp": "2.2"  # Manifest is incorrect.
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://blah.gaiamobile.org/manifest.webapp": "/de/blah",
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    }
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "not_allowed", ))

    def test_langpack_role_need_languages_provided(self):
        """Test that a language-provided is needed for langpacks."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.2"
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "missing_req_cond", ))

    def test_langpack_invalid_languages_provided_should_not_be_empty(self):
        """Test that language-provided is not empty."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.2"
            },
            "languages-provided": {}
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "empty", ))

    def test_langpack_invalid_languages_provided_language_should_be_dict(self):
        """Test that language-provided children are dicts."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.2"
            },
            "languages-provided": {
                "de": []
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "bad_type", ))

    def test_langpack_invalid_languages_provided_need_revision(self):
        """Test that language-provided revision is present."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.2"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "apps": {
                      "app://blah.gaiamobile.org/manifest.webapp": "/de/blah",
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    }
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "missing_req", ))

    def test_langpack_invalid_languages_provided_need_apps(self):
        """Test that language-provided apps is present."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.2"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "missing_req", ))

    def test_langpack_invalid_languages_provided_apps(self):
        """Test that language-provided apps should be a dict."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.2"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": ["app://blah.gaiamobile.org/manifest.webapp"]
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "bad_type", ))

    def test_langpack_invalid_languages_provided_apps_empty(self):
        """Test that language-provided apps should be non-empty dict."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.2"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                    }
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "empty", ))

    def test_langpack_invalid_languages_provided_revision(self):
        """Test that language-provided revision should be an int."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.2"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": "201411051234",  # Wrong type, should be a int.
                    "apps": {
                      "app://blah.gaiamobile.org/manifest.webapp": "/de/blah",
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    }
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "bad_type", ))

    def test_valid_langpack(self):
        """Test that a valid langpack passes validation."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.2"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://blah.gaiamobile.org/manifest.webapp": "/de/blah",
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    }
                }
            },
        })
        self.analyze()
        self.assert_silent()

    def test_valid_langpack_30(self):
        """Test that a valid langpack for FxOS 3.0 passes validation."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "3.0"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://blah.gaiamobile.org/manifest.webapp": "/de/blah",
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    }
                }
            },
        })
        self.analyze()
        self.assert_silent()

    def test_languages_target_invalid_for_webapps(self):
        """Test that language-target is invalid for non-langpack webapps."""
        self.data.update({
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.2"
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(
            ("spec", "webapp", "languages_target_langpacks", ))

    def test_languages_provided_invalid_for_webapps(self):
        """Test that language-provided is invalid for non-langpack webapps."""
        self.data.update({
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://blah.gaiamobile.org/manifest.webapp": "/de/blah",
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    }
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(
            ("spec", "webapp", "languages_provided_langpacks", ))

    def test_langpack_valid_speechdata(self):
        """Test that valid speech-data passes validation."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.5"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    },
                    "speech-data": [
                        "/de/speech-data/feat.params",
                        "/de/speech-data/de.dic",
                        "/de/speech-data/de.dic.dmp",
                        "/de/speech-data/mdef",
                        "/de/speech-data/means",
                        "/de/speech-data/mixture_weights",
                        "/de/speech-data/noisedict",
                        "/de/speech-data/sendump",
                        "/de/speech-data/transition_matrices",
                        "/de/speech-data/variances"
                    ]
                }
            },
        })
        self.analyze()
        self.assert_silent()

    def test_langpack_invalid_speechdata_empty(self):
        """Test that speech-data should not be empty."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.5"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    },
                    "speech-data": []
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "empty", ))

    def test_langpack_invalid_speechdata_type(self):
        """Test that speech-data should be an array."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.5"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    },
                    "speech-data": {}
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "bad_type", ))

    def test_langpack_invalid_speechdata_descendants_empty(self):
        """Test that speech-data descendants should not be empty."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.5"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    },
                    "speech-data": [
                        "",
                        "/de/speech-data/feat.params",
                    ]
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "empty", ))

    def test_langpack_invalid_speechdata_descendants_type(self):
        """Test that speech-data descendants should be strings."""
        self.resources.append(('packaged', True))
        self.data.update({
            "role": "langpack",
            "languages-target": {
                "app://*.gaiamobile.org/manifest.webapp": "2.5"
            },
            "languages-provided": {
                "de": {
                    "name": "Deutsch",
                    "revision": 201411051234,
                    "apps": {
                      "app://email.gaiamobile.org/manifest.webapp": "/de/email"
                    },
                    "speech-data": [
                        1
                    ]
                }
            },
        })
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "iterate", "bad_type", ))

    def test_invalid_role(self):
        """Test that app may not contain invalid role element."""
        self.data["role"] = "hello"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_empty_name(self):
        """Test that empty names are not allowed"""
        self.data["name"] = None
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_maxlengths(self):
        """Test that certain elements are capped in length."""
        self.data["name"] = "%" * 129
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_invalid_keys(self):
        """Test that unknown elements are flagged"""
        self.data["foobar"] = "hello"
        self.analyze()
        self.assert_failed(with_warnings=True)

    def test_warn_extra_keys(self):
        """Test that extra keys are flagged."""
        self.data["locales"]["es"]["foo"] = "hello"
        self.analyze()
        self.assert_failed(with_warnings=True)

    def test_icons_not_dict(self):
        """Test that the icons property is a dictionary."""
        self.data["icons"] = ["data:foo/bar.png"]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_icons_empty(self):
        """Test that no icons doesn't cause a traceback."""
        self.data["icons"] = {}
        self.analyze()

    def test_icons_size(self):
        """Test that webapp icon sizes must be integers."""
        self.data["icons"]["foo"] = "/foo.png"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_icons_data_url(self):
        """Test that webapp icons can be data URLs."""
        self.data["icons"]["128"] = "data:foo/bar.png"
        self.analyze()
        self.assert_silent()

    def test_icons_relative_url(self):
        """Test that webapp icons cannot be relative URLs."""
        self.data["icons"]["128"] = "foo/bar"
        self.analyze()
        self.assert_silent()

    def test_icons_absolute_url(self):
        """Test that webapp icons can be absolute URLs."""
        def test_icon(self, icon):
            self.setUp()
            self.data["icons"]["128"] = icon
            self.analyze()
            self.assert_silent()

        for icon in ['/foo/bar', 'http://foo.com/bar', 'https://foo.com/bar']:
            yield test_icon, self, icon

    def test_icons_has_min_selfhosted(self):
        del self.data["icons"]["128"]
        self.analyze()
        self.assert_silent()

    def test_icons_has_min_listed(self):
        self.listed = True
        self.data["installs_allowed_from"] = \
                appvalidator.constants.DEFAULT_WEBAPP_MRKT_URLS
        del self.data["icons"]["128"]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_no_locales(self):
        """Test that locales are not required."""
        del self.data["locales"]
        self.analyze()
        self.assert_silent()

    def test_no_default_locale_no_locales(self):
        """Test that locales are not required if no default_locale."""
        del self.data["default_locale"]
        del self.data["locales"]
        self.analyze()
        self.assert_silent()

    def test_no_default_locale(self):
        """Test that locales require default_locale."""
        del self.data["default_locale"]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_invalid_locale_keys(self):
        """Test that locales only contain valid keys."""
        # Banned locale element.
        self.data["locales"]["es"]["default_locale"] = "foo"
        self.analyze()
        self.assert_failed(with_warnings=True)

    def test_invalid_locale_keys_missing(self):
        """Test that locales aren't missing any required elements."""
        del self.data["locales"]["es"]["name"]
        self.analyze()
        self.assert_silent()

    def test_installs_allowed_from_not_list(self):
        """Test that the installs_allowed_from path is a list."""
        self.data["installs_allowed_from"] = "foobar"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_bad_installs_allowed_from_path(self):
        """Test that the installs_allowed_from path is valid."""
        self.data["installs_allowed_from"].append("foo/bar")
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_no_amo_installs_allowed_from(self):
        """Test that installs_allowed_from should include Marketplace."""
        # self.data does not include a marketplace URL by default.
        self.listed = True
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_amo_iaf(self):
        """Test that the various Marketplace URLs work."""

        # Test that the Marketplace production URL is acceptable.
        self.setUp()
        orig_iaf = self.data["installs_allowed_from"]

        def test_iaf(self, iaf, url):
            self.setUp()
            self.data["installs_allowed_from"] = iaf + [url]
            self.analyze()
            self.assert_silent()

        for url in appvalidator.constants.DEFAULT_WEBAPP_MRKT_URLS:
            yield test_iaf, self, orig_iaf, url

    def test_iaf_wildcard(self):
        """Test that installs_allowed_from can contain a wildcard."""
        self.listed = True
        self.data["installs_allowed_from"].append("*")
        self.analyze()
        self.assert_silent()

    def test_installs_allowed_from_protocol(self):
        """
        Test that if the developer includes a URL in the `installs_allowed_from`
        parameter that is a valid Marketplace URL but uses HTTP instead of
        HTTPS, we flag it as using the wrong protocol and not as an invalid URL.
        """
        self.listed = True
        bad_url = appvalidator.constants.DEFAULT_WEBAPP_MRKT_URLS[0].replace(
                "https", "http")

        self.data["installs_allowed_from"] = (bad_url, )
        self.analyze()
        self.assert_failed(with_errors=True)
        self.assert_got_errid(("spec", "webapp", "iaf_bad_mrkt_protocol", ))

    def test_launch_path_packaged(self):
        """Test that the launch path is present in a packaged app."""
        del self.data["launch_path"]
        self.resources.append(('packaged', True))
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_launch_path_not_string(self):
        """Test that the launch path is a string."""
        self.data["launch_path"] = [123]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_bad_launch_path(self):
        """Test that the launch path is valid."""
        self.data["launch_path"] = "data:asdf"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_bad_launch_path_protocol(self):
        """Test that the launch path cannot have a protocol."""
        self.data["launch_path"] = "http://foo.com/bar"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_bad_launch_path_absolute(self):
        """Test that the launch path is absolute."""
        self.data["launch_path"] = "/foo/bar"
        self.analyze()
        self.assert_silent()

    def test_widget_deprecated(self):
        """Test that the widget property is deprecated."""
        self.data["widget"] = {
            "path": "/butts.html",
            "width": 100,
            "height": 200
        }
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_dev_missing(self):
        """Test that the developer property cannot be absent."""
        del self.data["developer"]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_dev_not_dict(self):
        """Test that the developer property must be a dict."""
        self.data["developer"] = "foo"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_bad_dev_keys(self):
        """Test that the developer keys are present."""
        del self.data["developer"]["name"]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_bad_dev_url(self):
        """Test that the developer keys are correct."""
        self.data["developer"]["url"] = "foo"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_screen_size_missing(self):
        """Test that the 'screen_size' property can be absent."""
        del self.data["screen_size"]
        self.analyze()
        self.assert_silent()

    def test_screen_size_is_dict(self):
        """Test that the 'screen_size' property must be a dict."""
        self.data["screen_size"] = "foo"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_screen_size_contains_pair(self):
        """Test that 'screen_size' must contain at least one key/value pair."""
        self.data["screen_size"] = {}
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_bad_screen_size_key(self):
        """Test that the 'screen_size' keys are correct."""
        self.data["screen_size"]["max_width"] = "500"
        self.analyze()
        self.assert_failed(with_warnings=True)

    def test_bad_screen_size_value(self):
        """Test that the 'screen_size' keys are correct."""
        self.data["screen_size"]["min_width"] = "500px"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_required_screen_size_missing(self):
        """Test that the 'screen_size' property can be absent."""
        del self.data["screen_size"]
        self.analyze()
        self.assert_silent()

    def test_required_features_is_list(self):
        """Test that the 'required_features' property must be a list."""
        self.data["required_features"] = "fart"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_required_features_missing(self):
        """Test that 'required_features' can be absent."""
        del self.data["required_features"]
        self.analyze()
        self.assert_silent()

    def test_required_features_empty(self):
        """Test that 'required_features' can be an empty list."""
        self.data["required_features"] = []
        self.analyze()
        self.assert_silent()

    def test_orientation_missing(self):
        """Test that the 'orientation' property can be absent."""
        del self.data["orientation"]
        self.analyze()
        self.assert_silent()

    def test_orientation_list(self):
        """Test that the 'orientation' property can be absent."""
        self.data["orientation"] = ["portrait", "portrait-secondary"]
        self.analyze()
        self.assert_silent()

    def test_orientation_is_string(self):
        """Test that the 'orientation' property must be a string."""
        self.data["orientation"] = {}
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_orientation_cannot_be_empty(self):
        """Test that 'orientation' cannot be an empty string."""
        self.data["orientation"] = ""
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_orientation_valid_value(self):
        """Test that 'orientation' must have a valid value."""
        def test_orientation(self, orientation):
            self.setUp()
            self.data["orientation"] = orientation
            self.analyze()
            self.assert_silent()

        for key in ("portrait", "landscape", "portrait-secondary",
                    "landscape-secondary", "portrait-primary",
                    "landscape-primary"):
            yield test_orientation, self, key

    def test_orientation_bad_value(self):
        """Test that 'orientation' cannot have an invalid value."""
        self.data["orientation"] = "fart"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_orientation_empty_list(self):
        """Test that 'orientation' cannot be an empty list."""
        self.data["orientation"] = []
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_orientation_list_invalid(self):
        """Test that 'orientation' cannot be a list with invalid values."""
        self.data["orientation"] = ["fart"]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_orientation_list_mixed(self):
        """Test that 'orientation' cannot be a list with mixed values."""
        self.data["orientation"] = ["portrait", "fart", "landscape"]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_orientation_list_type(self):
        """Test that 'orientation' cannot be a list with non-strings."""
        self.data["orientation"] = ["portrait", 4]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_inputs_dict_valid(self):
        """Test with 'inputs' entries throw no errors."""
        self.data['inputs'] = {
            'input1': {
                'name': 'Symbols',
                'description': 'Symbols Virtual Keyboard',
                'launch_path': '/input1.html',
                'types': ['text']
            },
            'siri': {
                'name': 'Voice Control',
                'description': 'Voice Control Input',
                'launch_path': '/vc.html',
                'types': ['text', 'url']
            }
        }
        self.analyze()
        self.assert_silent()

    def test_inputs_dict_empty(self):
        """Test that 'inputs' may not be empty dict."""
        self.data['inputs'] = {}
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_inputs_dict_entry_missing_name(self):
        """Test that 'inputs' with an entry missing 'name'."""
        self.data['inputs'] = {
            'input1': {
                'description': 'Symbols Virtual Keyboard',
                'launch_path': '/input1.html',
                'types': ['text']
            }
        }
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_inputs_dict_entry_missing_description(self):
        """Test that 'inputs' with an entry missing 'description'."""
        self.data['inputs'] = {
            'input1': {
                'name': 'Symbols',
                'launch_path': '/input1.html',
                'types': ['text']
            }
        }
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_inputs_dict_entry_missing_launch_path(self):
        """Test that 'inputs' with an entry missing 'launch_path'."""
        self.data['inputs'] = {
            'input1': {
                'name': 'Symbols',
                'description': 'Symbols Virtual Keyboard',
                'types': ['text']
            }
        }
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_inputs_dict_entry_missing_types(self):
        """Test that 'inputs' with an entry missing 'types'."""
        self.data['inputs'] = {
            'input1': {
                'name': 'Symbols',
                'description': 'Symbols Virtual Keyboard',
                'launch_path': '/input1.html'
            }
        }
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_inputs_dict_entry_empty_types(self):
        """Test that 'inputs' with an entry with empty 'types'."""
        self.data['inputs'] = {
            'input1': {
                'name': 'Symbols',
                'description': 'Symbols Virtual Keyboard',
                'launch_path': '/input1.html',
                'types': []
            }
        }
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_inputs_dict_entry_invalid_types(self):
        """Test that 'inputs' with an entry with invalid 'types'."""
        self.data['inputs'] = {
            'input1': {
                'name': 'Symbols',
                'description': 'Symbols Virtual Keyboard',
                'launch_path': '/input1.html',
                'types': ['foo']
            }
        }
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_inputs_dict_entry_locales(self):
        """Test that 'inputs' with an localized entry."""
        self.data['inputs'] = {
            'input1': {
                'name': 'Symbols',
                'description': 'Symbols Virtual Keyboard',
                'launch_path': '/input1.html',
                'types': ['text'],
                'locales': {
                    'es': {
                        'name': 'foo',
                        'description': 'bar'
                    }
                }
            }
        }
        self.analyze()
        self.assert_silent()

    def test_inputs_dict_entry_invalid_locales(self):
        """Test that 'inputs' with an localized entry but contain invalid element."""
        self.data['inputs'] = {
            'input1': {
                'name': 'Symbols',
                'description': 'Symbols Virtual Keyboard',
                'launch_path': '/input1.html',
                'types': ['text'],
                'locales': {
                    'es': {
                        'name': 'foo',
                        'description': 'bar',
                        'foo': 'bar2'
                    }
                }
            }
        }
        self.analyze()
        self.assert_failed(with_warnings=True)

    def test_fullscreen_missing(self):
        """Test that the 'fullscreen' property can be absent."""
        del self.data["fullscreen"]
        self.analyze()
        self.assert_silent()

    def test_fullscreen_is_string(self):
        """Test that the 'fullscreen' property must be a string."""
        self.data["fullscreen"] = {}
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_fullscreen_cannot_be_empty(self):
        """Test that 'fullscreen' cannot be an empty string."""
        self.data["fullscreen"] = ""
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_fullscreen_valid_value(self):
        """Test that 'fullscreen' must have a valid value."""
        def test_fullscreen(self, value):
            self.setUp()
            self.data["fullscreen"] = key
            self.analyze()
            self.assert_silent()

        for key in ("true", "false", ):
            yield test_fullscreen, self, key

    def test_fullscreen_bad_value(self):
        """Test that 'fullscreen' cannot have an invalid value."""
        self.data["fullscreen"] = "fart"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_type_failed(self):
        """Test that the `type` element must be a recognized value."""

        self.data["type"] = "foo"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_type_valid(self):
        """Test that the `type` element doesn't fail with valid values."""

        def wrap(self, value):
            self.setUp()
            self.resources.append(("packaged", value != "web"))
            self.data["type"] = value
            self.analyze()
            self.assert_silent()

        for key in ("web", "privileged", "certified", ):
            yield wrap, self, key

    def test_type_not_certified(self):
        """Test that certified apps cannot be listed in the marketplace."""
        self.listed = True
        self.data["type"] = "certified"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_type_web_priv_fail(self):
        """Test that web apps cannot be privileged or certified."""
        self.data["type"] = "web"
        self.resources.append(("packaged", False))
        self.analyze()
        self.assert_silent()

    def test_type_packaged_priv_fail(self):
        """Test that web apps cannot be privileged or certified."""
        self.data["type"] = "privileged"
        self.resources.append(("packaged", True))
        self.analyze()
        self.assert_silent()

    ###########
    # Web activities are tested in tests/test_webapp_activity.py
    ###########

    def test_act_root_type(self):
        """Test that the most basic web activity passes."""

        self.data["activities"] = "wrong type"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_version(self):
        """Test that the version matches the format that we require."""
        def wrap(version, passes):
            self.setUp()
            self.data["version"] = version
            self.analyze()
            if passes:
                self.assert_silent()
            else:
                self.assert_failed(with_errors=True)

        yield wrap, "1.0", True
        yield wrap, "1.0.1", True
        yield wrap, "Poop", True
        yield wrap, "1.0b", True
        yield wrap, "*.*", True
        yield wrap, "1.5-alpha", True
        yield wrap, "1.5_windows", True
        yield wrap, "1.5_windows,x64", True
        yield wrap, "Mountain Lion", False
        yield wrap, "", False
        for char in "`~!@#$%^&()+=/|\\<>":
            yield wrap, char * 3, False

    def set_permissions(self):
        """Fill out the permissions node with every possible permission."""
        self.data["permissions"] = {}
        for perm in appvalidator.constants.ALL_PERMISSIONS:
            self.data["permissions"][perm] = {
                "description": "Required to make things good."
            }
            if perm in WebappSpec.PERMISSIONS_ACCESS:
                self.data["permissions"][perm]["access"] = (
                    WebappSpec.PERMISSIONS_ACCESS[perm][0])

    def test_permissions_full(self):
        self.set_permissions()
        self.analyze()
        self.assert_silent()
        for perm in appvalidator.constants.ALL_PERMISSIONS:
            self.assert_has_permission(perm)

    def test_permissions_extra_invalid(self):
        self.set_permissions()
        self.data["permissions"]["foo"] = {"description": "lol"}
        self.analyze()
        self.assert_failed(with_errors=True)
        assert 'foo' not in self.err.get_resource("permissions")
        for perm in appvalidator.constants.ALL_PERMISSIONS:
            self.assert_has_permission(perm)

    def test_permissions_missing_desc(self):
        self.set_permissions()
        self.data["permissions"]["alarm"] = {}
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_permissions_missing_access(self):
        self.set_permissions()
        del self.data["permissions"]["contacts"]["access"]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_permissions_invalid_access(self):
        self.set_permissions()
        self.data["permissions"]["contacts"]["access"] = "asdf"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_permissions_wrong_access(self):
        self.set_permissions()
        # This access type isn't available for the `settings` permission.
        self.data["permissions"]["settings"]["access"] = "createonly"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_permissions_mobileid(self):
        self.set_permissions()
        self.data["permissions"]["mobileid"] = {"description": "cause"}
        self.analyze()
        self.assert_silent()

    def test_csp(self):
        self.data['csp'] = 'this is the csp policy. it can be a string.'
        self.analyze()
        self.assert_silent()

    def test_description_long(self):
        self.data['description'] = 'x' * 1025
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_locale_description_long(self):
        self.data['locales']['es']['description'] = u'×' * 1025
        self.analyze()
        self.assert_failed(with_errors=True)
        assert 'locales > es > description' in (
            self.err.errors[0]['description'][-1])

    def test_appcache_path_packaged(self):
        self.data["appcache_path"] = '/foo.bar'
        self.analyze()
        self.assert_silent()

        self.resources.append(("packaged", True))
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_messages_not_list(self):
        self.data['messages'] = "foo"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_messages_obj_not_obj(self):
        self.data['messages'] = ["foo"]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_messages_multiple_keys(self):
        self.data['messages'] = [{"a": "1", "b": "2"}]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_messages_pass(self):
        self.data['messages'] = [{"key": "val"}, {"key": "val"}]
        self.analyze()
        self.assert_silent()

    def test_redirects_pass(self):
        self.data['redirects'] = [
            {"to": "asdf", "from": "qwer"},
            {"to": "asdf", "from": "qwer"},
        ]
        self.analyze()
        self.assert_silent()

    def test_redirects_type(self):
        self.data['redirects'] = 'asdf'
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_redirects_subtype(self):
        self.data['redirects'] = [
            'asdf',
            {"to": "asdf", "from": "qwer"},
        ]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_redirects_required_nodes(self):
        self.data['redirects'] = [
            {"bar": "asdf", "foo": "qwer"},
            {"to": "asdf", "from": "qwer"},
        ]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_redirects_missing_nodes(self):
        self.data['redirects'] = [
            {"to": "asdf"},
            {"to": "asdf", "from": "qwer"},
        ]
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_origin_unprivileged(self):
        self.data['origin'] = 'app://domain.com'
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_origin_must_be_lowercase(self):
        self.make_privileged()
        self.data['origin'] = 'app://DOMAIN.com'
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_arbitrary_origin(self):
        self.make_privileged()
        self.data['origin'] = 'app://just-some-identifier-string'
        self.analyze()
        self.assert_silent()

    def test_uuid_origin(self):
        self.make_privileged()
        self.data['origin'] = 'app://878a1076-130e-46fc-a73f-634394166d14'
        self.analyze()
        self.assert_silent()

    def test_origin_pass(self):
        self.make_privileged()
        self.data['origin'] = 'app://domain.com'
        self.analyze()
        self.assert_silent()

    def test_origin_dashes(self):
        self.make_privileged()
        self.data["origin"] = "app://my-domain.com"
        self.analyze()
        self.assert_silent()

    def test_origin_subdomains(self):
        self.make_privileged()
        self.data["origin"] = "app://sub.domain.com"
        self.analyze()
        self.assert_silent()

    def test_origin_non_fqdn(self):
        self.make_privileged()
        self.data["origin"] = "app://hello"
        self.analyze()
        self.assert_silent()

    def test_origin_type(self):
        self.make_privileged()
        self.data["origin"] = 123
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_origin_cannot_have_spaces(self):
        self.make_privileged()
        self.data["origin"] = "app://origin with spaces"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_origin_must_be_web_safe(self):
        self.make_privileged()
        self.data["origin"] = "app://control\tchars\ndisallowed"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_origin_must_have_app_protocol(self):
        self.make_privileged()
        self.data["origin"] = "random-identifier-without-protocol"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_origin_cannot_contain_path(self):
        self.make_privileged()
        self.data["origin"] = "app://domain.com/path"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_origin_cannot_end_in_trailing_slash(self):
        self.make_privileged()
        self.data["origin"] = "app://domain.com/"
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_origin_allowed(self):
        for origin in ("app://marketplace.firefox.com",
                       "app://gamescentre.mozilla.com",
                       "app://mozilla.org",
                       "app://system.gaiamobile.org"):
            self.make_privileged()
            self.data["origin"] = origin
            self.analyze()
            self.assert_silent()

    @patch("appvalidator.specs.webapps.BANNED_ORIGINS", [
        "gamescentre.mozilla.com",
        "firefox.com",
    ])
    def test_origin_banned(self):
        self.make_privileged()

        for origin in ("app://gamescentre.mozilla.com",
                       "app://theconsoleisdead.firefox.com"):
            self.data["origin"] = origin
            self.analyze()
            self.assert_failed(with_errors=True)

    def test_chrome(self):
        self.data["chrome"] = {"navigation": True}
        self.analyze()
        self.assert_silent()

    def test_chrome_alt(self):
        self.data["chrome"] = {"navigation": False}
        self.analyze()
        self.assert_silent()

    def test_chrome_bad_navigation(self):
        self.data["chrome"] = {"navigation": 123}
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_chrome_bad_keys(self):
        self.data["chrome"] = {"haldo": 123}
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_chrome_bad_type(self):
        self.data["chrome"] = []
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_precompile_wrong_format(self):
        # "precompile" should be list of files not this weird dict.
        self.data["precompile"] = {"foo.js": True}
        self.analyze()
        self.assert_failed(with_errors=True)

    def test_precompile_feature(self):
        self.analyze()
        self.assert_silent()
        self.assert_has_feature('PRECOMPILE_ASMJS')
