import re from textwrap import dedent import pytest from lxml.etree import Element, tostring as etree_tostring from streamlink.exceptions import PluginError, StreamlinkDeprecationWarning from streamlink.plugin.api import validate # noinspection PyProtectedMember from streamlink.plugin.api.validate._exception import ValidationError def assert_validationerror(exception, expected): assert str(exception) == dedent(expected).strip("\n") def test_text_is_str(recwarn: pytest.WarningsRecorder): assert "text" not in getattr(validate, "__dict__", {}) assert "text" in getattr(validate, "__all__", []) assert validate.text is str, "Exports text as str alias for backwards compatiblity" assert [(record.category, str(record.message), record.filename) for record in recwarn.list] == [ ( StreamlinkDeprecationWarning, "`streamlink.plugin.api.validate.text` is deprecated. Use `str` instead.", __file__, ), ] class TestSchema: @pytest.fixture(scope="class") def schema(self): return validate.Schema(str, "foo") @pytest.fixture(scope="class") def schema_nested(self, schema: validate.Schema): return validate.Schema(schema) def test_validate_success(self, schema: validate.Schema): assert schema.validate("foo") == "foo" def test_validate_failure(self, schema: validate.Schema): with pytest.raises(PluginError) as cm: schema.validate("bar") assert_validationerror(cm.value, """ Unable to validate result: ValidationError(equality): 'bar' does not equal 'foo' """) def test_validate_failure_custom(self, schema: validate.Schema): class CustomError(PluginError): pass with pytest.raises(CustomError) as cm: schema.validate("bar", name="data", exception=CustomError) assert_validationerror(cm.value, """ Unable to validate data: ValidationError(equality): 'bar' does not equal 'foo' """) def test_nested_success(self, schema_nested: validate.Schema): assert schema_nested.validate("foo") == "foo" def test_nested_failure(self, schema_nested: validate.Schema): with pytest.raises(PluginError) as cm: schema_nested.validate("bar") assert_validationerror(cm.value, """ Unable to validate result: ValidationError(equality): 'bar' does not equal 'foo' """) class TestEquality: def test_success(self): assert validate.validate("foo", "foo") == "foo" def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate("foo", "bar") assert_validationerror(cm.value, """ ValidationError(equality): 'bar' does not equal 'foo' """) class TestType: def test_success(self): class A: pass class B(A): pass a = A() b = B() assert validate.validate(A, a) is a assert validate.validate(B, b) is b assert validate.validate(A, b) is b def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(int, "1") assert_validationerror(cm.value, """ ValidationError(type): Type of '1' should be int, but is str """) class TestSequence: @pytest.mark.parametrize( ("schema", "value"), [ ([3, 2, 1, 0], [1, 2]), ((3, 2, 1, 0), (1, 2)), ({3, 2, 1, 0}, {1, 2}), (frozenset((3, 2, 1, 0)), frozenset((1, 2))), ], ids=[ "list", "tuple", "set", "frozenset", ], ) def test_sequences(self, schema, value): result = validate.validate(schema, value) assert result == value assert result is not value def test_empty(self): assert validate.validate([1, 2, 3], []) == [] def test_failure_items(self): with pytest.raises(ValidationError) as cm: validate.validate([1, 2, 3], [3, 4, 5]) assert_validationerror(cm.value, """ ValidationError(AnySchema): ValidationError(equality): 4 does not equal 1 ValidationError(equality): 4 does not equal 2 ValidationError(equality): 4 does not equal 3 """) def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate([1, 2, 3], {1, 2, 3}) assert_validationerror(cm.value, """ ValidationError(type): Type of {1, 2, 3} should be list, but is set """) class TestDict: def test_simple(self): schema = {"foo": "FOO", "bar": str} value = {"foo": "FOO", "bar": "BAR", "baz": "BAZ"} result = validate.validate(schema, value) assert result == {"foo": "FOO", "bar": "BAR"} assert result is not value @pytest.mark.parametrize( ("value", "expected"), [ ({"foo": "foo"}, {"foo": "foo"}), ({"bar": "bar"}, {}), ], ids=[ "existing", "missing", ], ) def test_optional(self, value, expected): assert validate.validate({validate.optional("foo"): "foo"}, value) == expected @pytest.mark.parametrize( ("schema", "value", "expected"), [ ( {str: {int: str}}, {"foo": {1: "foo"}}, {"foo": {1: "foo"}}, ), ( {validate.all(str, "foo"): str}, {"foo": "foo"}, {"foo": "foo"}, ), ( {validate.any(int, str): str}, {"foo": "foo"}, {"foo": "foo"}, ), ( {validate.transform(lambda s: s.upper()): str}, {"foo": "foo"}, {"FOO": "foo"}, ), ( {validate.union((str,)): str}, {"foo": "foo"}, {("foo", ): "foo"}, ), ], ids=[ "type", "AllSchema", "AnySchema", "TransformSchema", "UnionSchema", ], ) def test_keys(self, schema, value, expected): assert validate.validate(schema, value) == expected def test_failure_key(self): with pytest.raises(ValidationError) as cm: validate.validate({str: int}, {"foo": 1, 2: 3}) assert_validationerror(cm.value, """ ValidationError(dict): Unable to validate key Context(type): Type of 2 should be str, but is int """) def test_failure_key_value(self): with pytest.raises(ValidationError) as cm: validate.validate({str: int}, {"foo": "bar"}) assert_validationerror(cm.value, """ ValidationError(dict): Unable to validate value Context(type): Type of 'bar' should be int, but is str """) def test_failure_notfound(self): with pytest.raises(ValidationError) as cm: validate.validate({"foo": "bar"}, {"baz": "qux"}) assert_validationerror(cm.value, """ ValidationError(dict): Key 'foo' not found in {'baz': 'qux'} """) def test_failure_value(self): with pytest.raises(ValidationError) as cm: validate.validate({"foo": "bar"}, {"foo": 1}) assert_validationerror(cm.value, """ ValidationError(dict): Unable to validate value of key 'foo' Context(equality): 1 does not equal 'bar' """) def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate({}, 1) assert_validationerror(cm.value, """ ValidationError(type): Type of 1 should be dict, but is int """) class TestCallable: @staticmethod def subject(v): return v is not None def test_success(self): value = object() assert validate.validate(self.subject, value) is value def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(self.subject, None) assert_validationerror(cm.value, """ ValidationError(Callable): subject(None) is not true """) class TestPattern: @pytest.mark.parametrize(("pattern", "data", "expected"), [ (r"\s(?P\S+)\s", "foo bar baz", {"bar": "bar"}), (rb"\s(?P\S+)\s", b"foo bar baz", {"bar": b"bar"}), ]) def test_success(self, pattern, data, expected): result = validate.validate(re.compile(pattern), data) assert type(result) is re.Match assert result.groupdict() == expected def test_stringsubclass(self): assert validate.validate( validate.all( validate.xml_xpath_string(".//@bar"), re.compile(r".+"), validate.get(0), ), Element("foo", {"bar": "baz"}), ) == "baz" def test_failure(self): assert validate.validate(re.compile(r"foo"), "bar") is None def test_failure_type(self): with pytest.raises(ValidationError) as cm: validate.validate(re.compile(r"foo"), b"foo") assert_validationerror(cm.value, """ ValidationError(Pattern): cannot use a string pattern on a bytes-like object """) def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(re.compile(r"foo"), 123) assert_validationerror(cm.value, """ ValidationError(Pattern): Type of 123 should be str or bytes, but is int """) class TestAllSchema: @pytest.fixture(scope="class") def schema(self): return validate.all( str, lambda string: string.startswith("f"), "foo", ) def test_success(self, schema): assert validate.validate(schema, "foo") == "foo" @pytest.mark.parametrize( ("value", "error"), [ ( 123, """ ValidationError(type): Type of 123 should be str, but is int """, ), ( "bar", """ ValidationError(Callable): ('bar') is not true """, ), ( "failure", """ ValidationError(equality): 'failure' does not equal 'foo' """, ), ], ids=[ "first", "second", "third", ], ) def test_failure(self, schema, value, error): with pytest.raises(ValidationError) as cm: validate.validate(schema, value) assert_validationerror(cm.value, error) class TestAnySchema: @pytest.fixture(scope="class") def schema(self): return validate.any( "foo", str, lambda data: data is not None, ) @pytest.mark.parametrize( "value", [ "foo", "success", object(), ], ids=[ "first", "second", "third", ], ) def test_success(self, schema, value): assert validate.validate(schema, value) is value def test_failure(self, schema): with pytest.raises(ValidationError) as cm: validate.validate(schema, None) assert_validationerror(cm.value, """ ValidationError(AnySchema): ValidationError(equality): None does not equal 'foo' ValidationError(type): Type of None should be str, but is NoneType ValidationError(Callable): (None) is not true """) class TestNoneOrAllSchema: @pytest.mark.parametrize(("data", "expected"), [("foo", "FOO"), ("bar", None)]) def test_success(self, data, expected): assert validate.validate( validate.Schema( re.compile(r"foo"), validate.none_or_all( validate.get(0), validate.transform(str.upper), ), ), data, ) == expected def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.none_or_all(str, int), "foo") assert_validationerror(cm.value, """ ValidationError(NoneOrAllSchema): ValidationError(type): Type of 'foo' should be int, but is str """) class TestListSchema: def test_success(self): data = [1, 3.14, "foo"] result = validate.validate(validate.list(int, float, "foo"), data) assert result is not data assert result == [1, 3.14, "foo"] assert type(result) is type(data) assert len(result) == len(data) @pytest.mark.parametrize("data", [[1, "foo"], [1.2, "foo"], [1, "bar"], [1.2, "bar"]]) def test_success_subschemas(self, data): schema = validate.list( validate.any(int, float), validate.all(validate.any("foo", "bar"), validate.transform(str.upper)), ) result = validate.validate(schema, data) assert result is not data assert result[0] is data[0] assert result[1] is not data[1] assert result[1].isupper() def test_failure(self): data = [1, 3.14, "foo"] with pytest.raises(ValidationError) as cm: validate.validate(validate.list("foo", int, float), data) assert_validationerror(cm.value, """ ValidationError(ListSchema): ValidationError(equality): 1 does not equal 'foo' ValidationError(type): Type of 3.14 should be int, but is float ValidationError(type): Type of 'foo' should be float, but is str """) def test_failure_type(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.list(), {}) assert_validationerror(cm.value, """ ValidationError(ListSchema): Type of {} should be list, but is dict """) def test_failure_length(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.list("foo", "bar", "baz"), ["foo", "bar"]) assert_validationerror(cm.value, """ ValidationError(ListSchema): Length of list (2) does not match expectation (3) """) class TestRegexSchema: @pytest.mark.parametrize(("pattern", "data", "expected"), [ (r"\s(?P\S+)\s", "foo bar baz", {"bar": "bar"}), (rb"\s(?P\S+)\s", b"foo bar baz", {"bar": b"bar"}), ]) def test_success(self, pattern, data, expected): result = validate.validate(validate.regex(re.compile(pattern)), data) assert type(result) is re.Match assert result.groupdict() == expected def test_findall(self): assert validate.validate(validate.regex(re.compile(r"\w+"), "findall"), "foo bar baz") == ["foo", "bar", "baz"] def test_split(self): assert validate.validate(validate.regex(re.compile(r"\s+"), "split"), "foo bar baz") == ["foo", "bar", "baz"] def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.regex(re.compile(r"foo")), "bar") assert_validationerror(cm.value, """ ValidationError(RegexSchema): Pattern 'foo' did not match 'bar' """) def test_failure_type(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.regex(re.compile(r"foo")), b"foo") assert_validationerror(cm.value, """ ValidationError(RegexSchema): cannot use a string pattern on a bytes-like object """) def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.regex(re.compile(r"foo")), 123) assert_validationerror(cm.value, """ ValidationError(RegexSchema): Type of 123 should be str or bytes, but is int """) class TestTransformSchema: def test_success(self): def callback(string: str, *args, **kwargs): return string.format(*args, **kwargs) assert validate.validate( validate.transform(callback, "foo", "bar", baz="qux"), "{0} {1} {baz}", ) == "foo bar qux" def test_failure_signature(self): def callback(): pass # pragma: no cover with pytest.raises(TypeError) as cm: validate.validate( validate.transform(callback), "foo", ) assert str(cm.value).endswith("takes 0 positional arguments but 1 was given") def test_failure_schema(self): with pytest.raises(ValidationError) as cm: # noinspection PyTypeChecker validate.validate( validate.transform("not a callable"), "foo", ) assert_validationerror(cm.value, """ ValidationError(type): Type of 'not a callable' should be Callable, but is str """) class TestGetItemSchema: class Container: def __init__(self, exception): self.exception = exception def __getitem__(self, item): raise self.exception def __repr__(self): return self.__class__.__name__ @pytest.mark.parametrize( "obj", [ {"foo": "bar"}, Element("elem", {"foo": "bar"}), re.match(r"(?P.+)", "bar"), ], ids=[ "dict", "lxml.etree.Element", "re.Match", ], ) def test_simple(self, obj): assert validate.validate(validate.get("foo"), obj) == "bar" @pytest.mark.parametrize("exception", [KeyError, IndexError]) def test_getitem_no_default(self, exception): container = self.Container(exception()) assert validate.validate(validate.get("foo"), container) is None @pytest.mark.parametrize("exception", [KeyError, IndexError]) def test_getitem_default(self, exception): container = self.Container(exception("failure")) assert validate.validate(validate.get("foo", default="default"), container) == "default" @pytest.mark.parametrize("exception", [TypeError, AttributeError]) def test_getitem_error(self, exception): container = self.Container(exception("failure")) with pytest.raises(ValidationError) as cm: validate.validate(validate.get("foo", default="default"), container) assert_validationerror(cm.value, """ ValidationError(GetItemSchema): Could not get key 'foo' from object Container Context: failure """) def test_nested(self): dictionary = {"foo": {"bar": {"baz": "qux"}}} assert validate.validate(validate.get(("foo", "bar", "baz")), dictionary) == "qux" def test_nested_default(self): dictionary = {"foo": {"bar": {"baz": "qux"}}} assert validate.validate(validate.get(("foo", "bar", "qux"), default="default"), dictionary) == "default" def test_nested_failure(self): dictionary = {"foo": {"bar": {"baz": "qux"}}} with pytest.raises(ValidationError) as cm: validate.validate(validate.get(("foo", "qux", "baz"), default="default"), dictionary) assert_validationerror(cm.value, """ ValidationError(GetItemSchema): Item 'qux' was not found in object {'bar': {'baz': 'qux'}} """) def test_strict(self): dictionary = { ("foo", "bar", "baz"): "foo-bar-baz", "foo": {"bar": {"baz": "qux"}}, } assert validate.validate(validate.get(("foo", "bar", "baz"), strict=True), dictionary) == "foo-bar-baz" class TestAttrSchema: class Subject: foo = 1 bar = 2 def __repr__(self): return self.__class__.__name__ @pytest.fixture() def obj(self): obj1 = self.Subject() obj2 = self.Subject() obj1.bar = obj2 return obj1 def test_success(self, obj): schema = validate.attr({"foo": validate.transform(lambda num: num + 1)}) newobj = validate.validate(schema, obj) assert obj.foo == 1 assert newobj is not obj assert newobj.foo == 2 assert newobj.bar is obj.bar def test_failure_missing(self, obj): with pytest.raises(ValidationError) as cm: validate.validate(validate.attr({"missing": int}), obj) assert_validationerror(cm.value, """ ValidationError(AttrSchema): Attribute 'missing' not found on object Subject """) def test_failure_subschema(self, obj): with pytest.raises(ValidationError) as cm: validate.validate(validate.attr({"foo": str}), obj) assert_validationerror(cm.value, """ ValidationError(AttrSchema): Could not validate attribute 'foo' Context(type): Type of 1 should be str, but is int """) class TestXmlElementSchema: upper = validate.transform(str.upper) @pytest.fixture() def element(self): childA = Element("childA", {"a": "1"}) childB = Element("childB", {"b": "2"}) childC = Element("childC") childA.text = "childAtext" childA.tail = "childAtail" childB.text = "childBtext" childB.tail = "childBtail" childB.append(childC) parent = Element("parent", {"attrkey1": "attrval1", "attrkey2": "attrval2"}) parent.text = "parenttext" parent.tail = "parenttail" parent.append(childA) parent.append(childB) return parent @pytest.mark.parametrize( ("schema", "expected"), [ ( validate.xml_element(), ( "" + "parenttext" + "childAtext" + "childAtail" + "childBtext" + "childBtail" + "" + "parenttail" ), ), ( validate.xml_element(tag=upper, attrib={upper: upper}, text=upper, tail=upper), ( "" + "PARENTTEXT" + "childAtext" + "childAtail" + "childBtext" + "childBtail" + "" + "PARENTTAIL" ), ), ], ids=[ "empty", "subschemas", ], ) def test_success(self, element, schema, expected): newelement = validate.validate(schema, element) assert etree_tostring(newelement).decode("utf-8") == expected assert newelement is not element assert newelement[0] is not element[0] assert newelement[1] is not element[1] assert newelement[1][0] is not element[1][0] @pytest.mark.parametrize( ("schema", "error"), [ ( validate.xml_element(tag="invalid"), """ ValidationError(XmlElementSchema): Unable to validate XML tag Context(equality): 'parent' does not equal 'invalid' """, ), ( validate.xml_element(attrib={"invalid": "invalid"}), """ ValidationError(XmlElementSchema): Unable to validate XML attributes Context(dict): Key 'invalid' not found in {'attrkey1': 'attrval1', 'attrkey2': 'attrval2'} """, ), ( validate.xml_element(text="invalid"), """ ValidationError(XmlElementSchema): Unable to validate XML text Context(equality): 'parenttext' does not equal 'invalid' """, ), ( validate.xml_element(tail="invalid"), """ ValidationError(XmlElementSchema): Unable to validate XML tail Context(equality): 'parenttail' does not equal 'invalid' """, ), ], ids=[ "tag", "attrib", "text", "tail", ], ) def test_failure(self, element, schema, error): with pytest.raises(ValidationError) as cm: validate.validate(schema, element) assert_validationerror(cm.value, error) def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.xml_element(), "not-an-element") assert_validationerror(cm.value, """ ValidationError(Callable): iselement('not-an-element') is not true """) class TestUnionGetSchema: def test_simple(self): assert validate.validate( validate.union_get("foo", "bar"), {"foo": 1, "bar": 2}, ) == (1, 2) def test_sequence_type(self): assert validate.validate( validate.union_get("foo", "bar", seq=list), {"foo": 1, "bar": 2}, ) == [1, 2] def test_nested(self): assert validate.validate( validate.union_get( ("foo", "bar"), ("baz", "qux"), ), {"foo": {"bar": 1}, "baz": {"qux": 2}}, ) == (1, 2) class TestUnionSchema: upper = validate.transform(str.upper) def test_dict_success(self): schema = validate.union({ "foo": str, "bar": self.upper, validate.optional("baz"): int, }) assert validate.validate(schema, "value") == {"foo": "value", "bar": "VALUE"} def test_dict_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.union({"foo": int}), "value") assert_validationerror(cm.value, """ ValidationError(UnionSchema): Could not validate union Context(dict): Unable to validate union 'foo' Context(type): Type of 'value' should be int, but is str """) @pytest.mark.parametrize( ("schema", "expected"), [ (validate.union([str, upper]), ["value", "VALUE"]), (validate.union((str, upper)), ("value", "VALUE")), (validate.union({str, upper}), {"value", "VALUE"}), (validate.union(frozenset((str, upper))), frozenset(("value", "VALUE"))), ], ids=[ "list", "tuple", "set", "frozenset", ], ) def test_sequence(self, schema, expected): result = validate.validate(schema, "value") assert result == expected def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.union(None), None) assert_validationerror(cm.value, """ ValidationError(UnionSchema): Could not validate union Context: Invalid union type: NoneType """) class TestLengthValidator: @pytest.mark.parametrize( ("minlength", "value"), [(3, "foo"), (3, [1, 2, 3])], ) def test_success(self, minlength, value): assert validate.validate(validate.length(minlength), value) @pytest.mark.parametrize( ("minlength", "value"), [(3, "foo"), (3, [1, 2, 3])], ) def test_failure(self, minlength, value): with pytest.raises(ValidationError) as cm: validate.validate(validate.length(minlength + 1), value) assert_validationerror(cm.value, """ ValidationError(length): Minimum length is 4, but value is 3 """) class TestStartsWithValidator: def test_success(self): assert validate.validate(validate.startswith("foo"), "foo bar baz") def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.startswith("invalid"), "foo bar baz") assert_validationerror(cm.value, """ ValidationError(startswith): 'foo bar baz' does not start with 'invalid' """) def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.startswith("invalid"), 1) assert_validationerror(cm.value, """ ValidationError(type): Type of 1 should be str, but is int """) class TestEndsWithValidator: def test_success(self): assert validate.validate(validate.endswith("baz"), "foo bar baz") def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.endswith("invalid"), "foo bar baz") assert_validationerror(cm.value, """ ValidationError(endswith): 'foo bar baz' does not end with 'invalid' """) def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.endswith("invalid"), 1) assert_validationerror(cm.value, """ ValidationError(type): Type of 1 should be str, but is int """) class TestContainsValidator: def test_success(self): assert validate.validate(validate.contains("bar"), "foo bar baz") def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.contains("invalid"), "foo bar baz") assert_validationerror(cm.value, """ ValidationError(contains): 'foo bar baz' does not contain 'invalid' """) def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.contains("invalid"), 1) assert_validationerror(cm.value, """ ValidationError(type): Type of 1 should be str, but is int """) class TestUrlValidator: url = "https://user:pass@sub.host.tld:1234/path.m3u8?query#fragment" @pytest.mark.parametrize( "params", [ dict(scheme="http"), dict(scheme="https"), dict(netloc="user:pass@sub.host.tld:1234", username="user", password="pass", hostname="sub.host.tld", port=1234), dict(path=validate.endswith(".m3u8")), ], ids=[ "implicit https", "explicit https", "multiple attributes", "subschemas", ], ) def test_success(self, params): assert validate.validate(validate.url(**params), self.url) def test_failure_valid_url(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.url(), "foo") assert_validationerror(cm.value, """ ValidationError(url): 'foo' is not a valid URL """) def test_failure_url_attribute(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.url(invalid=str), self.url) assert_validationerror(cm.value, """ ValidationError(url): Invalid URL attribute 'invalid' """) def test_failure_subschema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.url(hostname="invalid"), self.url) assert_validationerror(cm.value, """ ValidationError(url): Unable to validate URL attribute 'hostname' Context(equality): 'sub.host.tld' does not equal 'invalid' """) def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.url(), 1) assert_validationerror(cm.value, """ ValidationError(type): Type of 1 should be str, but is int """) class TestGetAttrValidator: @pytest.fixture(scope="class") def subject(self): class Subject: foo = 1 return Subject() def test_simple(self, subject): assert validate.validate(validate.getattr("foo"), subject) == 1 def test_default(self, subject): assert validate.validate(validate.getattr("bar", 2), subject) == 2 def test_no_default(self, subject): assert validate.validate(validate.getattr("bar"), subject) is None assert validate.validate(validate.getattr("baz"), None) is None class TestHasAttrValidator: class Subject: foo = 1 def __repr__(self): return self.__class__.__name__ def test_success(self): assert validate.validate(validate.hasattr("foo"), self.Subject()) def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.hasattr("bar"), self.Subject()) assert_validationerror(cm.value, """ ValidationError(Callable): getter(Subject) is not true """) class TestFilterValidator: def test_dict(self): schema = validate.filter(lambda k, v: k < 2 and v > 0) value = {0: 0, 1: 1, 2: 0, 3: 1} assert validate.validate(schema, value) == {1: 1} def test_sequence(self): schema = validate.filter(lambda k: k < 2) value = (0, 1, 2, 3) assert validate.validate(schema, value) == (0, 1) class TestMapValidator: def test_dict(self): schema = validate.map(lambda k, v: (k + 1, v + 1)) value = {0: 0, 1: 1, 2: 0, 3: 1} assert validate.validate(schema, value) == {1: 1, 2: 2, 3: 1, 4: 2} def test_sequence(self): schema = validate.map(lambda k: k + 1) value = (0, 1, 2, 3) assert validate.validate(schema, value) == (1, 2, 3, 4) class TestXmlFindValidator: def test_success(self): element = Element("foo") assert validate.validate(validate.xml_find("."), element) is element def test_namespaces(self): root = Element("root") child = Element("{http://a}foo") root.append(child) assert validate.validate(validate.xml_find("./a:foo", namespaces={"a": "http://a"}), root) is child def test_failure_no_element(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.xml_find("*"), Element("foo")) assert_validationerror(cm.value, """ ValidationError(xml_find): ElementPath query '*' did not return an element """) def test_failure_not_found(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.xml_find("invalid"), Element("foo")) assert_validationerror(cm.value, """ ValidationError(xml_find): ElementPath query 'invalid' did not return an element """) def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.xml_find("."), "not-an-element") assert_validationerror(cm.value, """ ValidationError(Callable): iselement('not-an-element') is not true """) def test_failure_syntax(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.xml_find("["), Element("foo")) assert_validationerror(cm.value, """ ValidationError(xml_find): ElementPath syntax error: '[' Context: invalid path """) class TestXmlFindallValidator: @pytest.fixture(scope="class") def element(self): element = Element("root") for child in Element("foo"), Element("bar"), Element("baz"): element.append(child) return element def test_simple(self, element): assert validate.validate(validate.xml_findall("*"), element) == [element[0], element[1], element[2]] def test_empty(self, element): assert validate.validate(validate.xml_findall("missing"), element) == [] def test_namespaces(self): root = Element("root") for child in Element("{http://a}foo"), Element("{http://unknown}bar"), Element("{http://a}baz"): root.append(child) assert validate.validate(validate.xml_findall("./a:*", namespaces={"a": "http://a"}), root) == [root[0], root[2]] def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.xml_findall("*"), "not-an-element") assert_validationerror(cm.value, """ ValidationError(Callable): iselement('not-an-element') is not true """) class TestXmlFindtextValidator: def test_simple(self): element = Element("foo") element.text = "bar" assert validate.validate(validate.xml_findtext("."), element) == "bar" def test_empty(self): element = Element("foo") assert validate.validate(validate.xml_findtext("."), element) is None def test_namespaces(self): root = Element("root") child = Element("{http://a}foo") child.text = "bar" root.append(child) assert validate.validate(validate.xml_findtext("./a:foo", namespaces={"a": "http://a"}), root) == "bar" def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.xml_findtext("."), "not-an-element") assert_validationerror(cm.value, """ ValidationError(Callable): iselement('not-an-element') is not true """) class TestXmlXpathValidator: @pytest.fixture(scope="class") def element(self): element = Element("root") for child in Element("foo"), Element("bar"), Element("baz"): child.text = child.tag.upper() element.append(child) return element def test_simple(self, element): assert validate.validate(validate.xml_xpath("*"), element) == [element[0], element[1], element[2]] assert validate.validate(validate.xml_xpath("*/text()"), element) == ["FOO", "BAR", "BAZ"] def test_empty(self, element): assert validate.validate(validate.xml_xpath("invalid"), element) is None def test_other(self, element): assert validate.validate(validate.xml_xpath("local-name(.)"), element) == "root" def test_namespaces(self): nsmap = {"a": "http://a", "b": "http://b"} root = Element("root", nsmap=nsmap) for child in Element("{http://a}child"), Element("{http://b}child"): root.append(child) assert validate.validate(validate.xml_xpath("./b:child", namespaces=nsmap), root)[0] is root[1] def test_extensions(self, element): def foo(context, a, b): return int(context.context_node.attrib.get("val")) + a + b element = Element("root", attrib={"val": "3"}) assert validate.validate(validate.xml_xpath("foo(5, 7)", extensions={(None, "foo"): foo}), element) == 15.0 def test_smart_strings(self, element): assert validate.validate(validate.xml_xpath("*/text()"), element)[0].getparent().tag == "foo" assert not hasattr(validate.validate(validate.xml_xpath("*/text()", smart_strings=False), element)[0], "getparent") def test_variables(self, element): assert validate.validate(validate.xml_xpath("*[local-name() = $name]/text()", name="foo"), element) == ["FOO"] def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.xml_xpath("."), "not-an-element") assert_validationerror(cm.value, """ ValidationError(Callable): iselement('not-an-element') is not true """) def test_failure_evaluation(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.xml_xpath("?"), Element("root")) assert_validationerror(cm.value, """ ValidationError(xml_xpath): XPath evaluation error: '?' Context: Invalid expression """) class TestXmlXpathStringValidator: @pytest.fixture(scope="class") def element(self): element = Element("root") for child in Element("foo"), Element("bar"), Element("baz"): child.text = child.tag.upper() element.append(child) return element def test_simple(self, element): assert validate.validate(validate.xml_xpath_string("./foo/text()"), element) == "FOO" def test_empty(self, element): assert validate.validate(validate.xml_xpath_string("./text()"), element) is None def test_smart_strings(self, element): assert not hasattr(validate.validate(validate.xml_xpath_string("./foo/text()"), element)[0], "getparent") def test_failure_schema(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.xml_xpath_string("."), "not-an-element") assert_validationerror(cm.value, """ ValidationError(Callable): iselement('not-an-element') is not true """) class TestParseJsonValidator: def test_success(self): assert validate.validate( validate.parse_json(), """{"a": ["b", true, false, null, 1, 2.3]}""", ) == {"a": ["b", True, False, None, 1, 2.3]} def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.parse_json(), "invalid") assert_validationerror(cm.value, """ ValidationError: Unable to parse JSON: Expecting value: line 1 column 1 (char 0) ('invalid') """) class TestParseHtmlValidator: def test_success(self): assert validate.validate( validate.parse_html(), """"perfectly"valid
HTML""", ).tag == "html" def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.parse_html(), None) assert_validationerror(cm.value, """ ValidationError: Unable to parse HTML: can only parse strings (None) """) class TestParseXmlValidator: def test_success(self): assert validate.validate( validate.parse_xml(), """""", ).tag == "root" def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.parse_xml(), None) assert_validationerror(cm.value, """ ValidationError: Unable to parse XML: can only parse strings (None) """) class TestParseQsdValidator: def test_success(self): assert validate.validate( validate.parse_qsd(), "foo=bar&foo=baz&qux=quux", ) == {"foo": "baz", "qux": "quux"} def test_failure(self): with pytest.raises(ValidationError) as cm: validate.validate(validate.parse_qsd(), 123) assert_validationerror(cm.value, """ ValidationError: Unable to parse query string: 'int' object has no attribute 'decode' (123) """) class TestValidationError: def test_subclass(self): assert issubclass(ValidationError, ValueError) def test_empty(self): assert str(ValidationError()) == "ValidationError:" assert str(ValidationError("")) == "ValidationError:" assert str(ValidationError(ValidationError())) == "ValidationError:\n ValidationError:" assert str(ValidationError(ValidationError(""))) == "ValidationError:\n ValidationError:" def test_single(self): assert str(ValidationError("foo")) == "ValidationError:\n foo" assert str(ValidationError(ValueError("bar"))) == "ValidationError:\n bar" def test_single_nested(self): err = ValidationError(ValidationError("baz")) assert_validationerror(err, """ ValidationError: ValidationError: baz """) def test_multiple_nested(self): err = ValidationError( "a", ValidationError("b", "c"), "d", ValidationError("e"), "f", ) assert_validationerror(err, """ ValidationError: a ValidationError: b c d ValidationError: e f """) def test_context(self): errA = ValidationError("a") errB = ValidationError("b") errC = ValidationError("c") errA.__cause__ = errB errB.__cause__ = errC assert_validationerror(errA, """ ValidationError: a Context: b Context: c """) def test_multiple_nested_context(self): errAB = ValidationError("a", "b") errC = ValidationError("c") errDE = ValidationError("d", "e") errF = ValidationError("f") errG = ValidationError("g") errHI = ValidationError("h", "i") errCF = ValidationError(errC, errF) errAB.__cause__ = errCF errC.__cause__ = errDE errF.__cause__ = errG errCF.__cause__ = errHI assert_validationerror(errAB, """ ValidationError: a b Context: ValidationError: c Context: d e ValidationError: f Context: g Context: h i """) def test_schema(self): err = ValidationError( ValidationError( "foo", schema=dict, ), ValidationError( "bar", schema="something", ), schema=validate.any, ) assert_validationerror(err, """ ValidationError(AnySchema): ValidationError(dict): foo ValidationError(something): bar """) def test_recursion(self): err1 = ValidationError("foo") err2 = ValidationError("bar") err2.__cause__ = err1 err1.__cause__ = err2 assert_validationerror(err1, """ ValidationError: foo Context: bar Context: ... """) def test_truncate(self): err = ValidationError( "foo {foo} bar {bar} baz", foo="Some really long error message that exceeds the maximum error message length", bar=repr("Some really long error message that exceeds the maximum error message length"), ) assert_validationerror(err, """ ValidationError: foo bar <'Some really long error message that exceeds the maximu...> baz """) # noqa: 501