blob: 28d96309ad6e40ab7e4282bc0bac69984c2ab0f2 [file] [edit]
"""Tests for the machine-readable attributes carried by IDNAError."""
import codecs
import pickle
import re
import unittest
from typing import get_args
from unittest import mock
import idna
import idna.codec
import idna.core
class ErrorAttributeTests(unittest.TestCase):
def _cases(self):
"""(trigger, expected class, expected text, codepoint, position)"""
r = "\u05d0" # R
an = "\u0660" # AN
nsm = "\u0610" # NSM
return [
(lambda: idna.alabel("abc\u0141"), idna.InvalidCodepoint, "abc\u0141", 0x141, 4),
(lambda: idna.alabel("a\u200cb"), idna.InvalidCodepointContext, "a\u200cb", 0x200C, 2),
(lambda: idna.alabel("a\xb7b"), idna.InvalidCodepointContext, "a\xb7b", 0xB7, 2),
(lambda: idna.alabel("\u0301abc"), idna.IDNAError, "\u0301abc", 0x301, 1),
(lambda: self._unknown_direction("ab"), idna.IDNABidiError, "ab", 0x61, 1),
(lambda: idna.check_bidi(an + r), idna.IDNABidiError, an + r, 0x660, 1), # rule 1
(lambda: idna.check_bidi(r + "a"), idna.IDNABidiError, r + "a", 0x61, 2), # rule 2
(lambda: idna.check_bidi(r + "-" + nsm), idna.IDNABidiError, r + "-" + nsm, 0x2D, 2), # rule 3
(lambda: idna.check_bidi(r + an + "0"), idna.IDNABidiError, r + an + "0", 0x30, 3), # rule 4
(lambda: idna.alabel("a" + r), idna.IDNABidiError, "a" + r, 0x5D0, 2), # rule 5
(lambda: idna.check_bidi("a-", check_ltr=True), idna.IDNABidiError, "a-", 0x2D, 2), # rule 6
(lambda: idna.uts46_remap("a\x80"), idna.InvalidCodepoint, "a\x80", 0x80, 2),
(lambda: idna.uts46_remap("a_b"), idna.InvalidCodepoint, "a_b", 0x5F, 2),
(lambda: idna.uts46_remap("a\uff3fb"), idna.InvalidCodepoint, "a\uff3fb", 0xFF3F, 2),
(lambda: idna.encode("a_b", uts46=True, std3_rules=True), idna.InvalidCodepoint, "a_b", 0x5F, 2),
]
def test_attributes_are_populated(self):
for trigger, exc_class, text, codepoint, position in self._cases():
with self.subTest(text=text):
with self.assertRaises(exc_class) as ctx:
trigger()
err = ctx.exception
self.assertEqual(err.text, text)
self.assertEqual(err.codepoint, codepoint)
self.assertEqual(err.position, position)
# position indexes text and names the codepoint ...
self.assertEqual(ord(err.text[err.position - 1]), err.codepoint)
# ... and agrees with the message where the message quotes one
match = re.search(r"position (\d+)", str(err))
if match:
self.assertEqual(int(match.group(1)), err.position)
def test_every_error_code_is_raisable(self):
"""Each code has an input that produces it, and vice versa."""
r = "\u05d0"
an = "\u0660"
triggers = {
"input_too_long": lambda: idna.encode("a" * 1025),
"label_too_long": lambda: idna.alabel("a" * 64),
"domain_too_long": lambda: idna.encode(".".join(["a" * 63] * 4)),
"empty_label": lambda: idna.encode("a..b"),
"empty_domain": lambda: idna.encode(""),
"not_nfc": lambda: idna.alabel("e\u0301xample"),
"hyphen_3_4": lambda: idna.alabel("ab--cd"),
"hyphen_start_end": lambda: idna.alabel("-abc"),
"leading_combiner": lambda: idna.alabel("\u0301abc"),
"disallowed_codepoint": lambda: idna.alabel("abc\u0141"),
"contextj": lambda: idna.alabel("a\u200cb"),
"contexto": lambda: idna.alabel("a\xb7b"),
"unknown_codepoint": self._unknown_codepoint,
"bidi_rule_1": lambda: idna.check_bidi(an + r),
"bidi_rule_2": lambda: idna.check_bidi(r + "a"),
"bidi_rule_3": lambda: idna.check_bidi(r + "-"),
"bidi_rule_4": lambda: idna.check_bidi(r + an + "0"),
"bidi_rule_5": lambda: idna.alabel("a" + r),
"bidi_rule_6": lambda: idna.check_bidi("a-", check_ltr=True),
"bidi_unknown_direction": lambda: self._unknown_direction("ab"),
"invalid_alabel": lambda: idna.ulabel("xn--"),
"non_canonical_alabel": lambda: idna.ulabel("xn---bbk"),
"invalid_ascii": lambda: idna.encode(b"\xff"),
"invalid_utf8": lambda: idna.check_label(b"\xff"),
"uts46_disallowed": lambda: idna.uts46_remap("a\x80"),
"uts46_std3": lambda: idna.uts46_remap("a_b"),
"unsupported_errors": lambda: codecs.encode("a", "idna2008", errors="ignore"),
}
codes = set(get_args(idna.core._ErrorCode))
self.assertEqual(set(triggers), codes)
for code, trigger in triggers.items():
with self.subTest(code=code):
with self.assertRaises(idna.IDNAError) as ctx:
trigger()
self.assertEqual(ctx.exception.code, code)
# Conditions that depend on the host's Unicode database being older than
# the input cannot be triggered portably (from Python 3.15,
# ``unicodedata.bidirectional`` returns a default class for every
# codepoint), so simulate them.
def _unknown_codepoint(self):
with mock.patch("idna.core._combining_class", side_effect=ValueError):
idna.check_label("a\u200cb")
def _unknown_direction(self, label):
with mock.patch("idna.core.unicodedata.bidirectional", return_value=""):
idna.check_bidi(label)
def test_unknown_codepoint_adjacent_to_joiner(self):
unknown = mock.patch("idna.core._combining_class", side_effect=ValueError)
with unknown, self.assertRaises(idna.IDNAError) as ctx:
idna.check_label("a\u200cb")
err = ctx.exception
self.assertNotIsInstance(err, idna.InvalidCodepointContext)
self.assertEqual(err.code, "unknown_codepoint")
self.assertEqual((err.text, err.codepoint, err.position), ("a\u200cb", 0x200C, 2))
def test_positional_attributes_default_to_none(self):
for trigger in (
lambda: idna.encode("a..b"),
lambda: idna.encode(""),
lambda: idna.alabel("a" * 64),
lambda: idna.alabel("ab--cd"),
lambda: idna.alabel("-abc"),
lambda: idna.ulabel("xn--"),
lambda: idna.encode(b"\xff"),
):
with self.assertRaises(idna.IDNAError) as ctx:
trigger()
err = ctx.exception
self.assertIsNotNone(err.code)
self.assertIsNone(err.text)
self.assertIsNone(err.codepoint)
self.assertIsNone(err.position)
def test_construction_is_backwards_compatible(self):
err = idna.IDNAError("just a message")
self.assertEqual(str(err), "just a message")
self.assertEqual(err.args, ("just a message",))
self.assertIsNone(err.code)
self.assertIsNone(err.text)
self.assertEqual(idna.IDNAError().args, ())
self.assertEqual(idna.InvalidCodepoint("a", "b").args, ("a", "b"))
def test_message_remains_sole_positional_argument(self):
with self.assertRaises(idna.InvalidCodepoint) as ctx:
idna.alabel("abc\u0141")
self.assertEqual(ctx.exception.args, ("Codepoint U+0141 at position 4 of 'abc\u0141' not allowed",))
def test_attributes_survive_pickle(self):
with self.assertRaises(idna.InvalidCodepoint) as ctx:
idna.alabel("abc\u0141")
err = pickle.loads(pickle.dumps(ctx.exception))
self.assertIsInstance(err, idna.InvalidCodepoint)
self.assertEqual(str(err), str(ctx.exception))
self.assertEqual(err.code, "disallowed_codepoint")
self.assertEqual((err.text, err.codepoint, err.position), ("abc\u0141", 0x141, 4))
if __name__ == "__main__":
unittest.main()