| """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() |