diff --git a/pydantic_br_validator/__init__.py b/pydantic_br_validator/__init__.py index 3312eea..51cb255 100644 --- a/pydantic_br_validator/__init__.py +++ b/pydantic_br_validator/__init__.py @@ -25,5 +25,6 @@ CEPDigits = str else: from .fields.cnpj_field import * # noqa + from .fields.cnh_field import * # noqa from .fields.cpf_field import * # noqa from .fields.cep_field import * # noqa diff --git a/pydantic_br_validator/fields/cnh_field.py b/pydantic_br_validator/fields/cnh_field.py new file mode 100644 index 0000000..a2ca7b5 --- /dev/null +++ b/pydantic_br_validator/fields/cnh_field.py @@ -0,0 +1,16 @@ +from ..validators.cnh_validator import CNHValidator +from .base_field import BaseDigits + +__all__ = ["CNH"] + + +class CNH(BaseDigits): + """ + Only Accepts string of CNH with digits. + + Attributes: + number (str): CNH number. + """ + + format = "cnh" + Validator = CNHValidator diff --git a/pydantic_br_validator/validators/cnh_validator.py b/pydantic_br_validator/validators/cnh_validator.py new file mode 100644 index 0000000..26e7b91 --- /dev/null +++ b/pydantic_br_validator/validators/cnh_validator.py @@ -0,0 +1,50 @@ +import re + +from .base_validator import FieldValidator + +__all__ = ["CNHValidator"] + + +class CNHValidator(FieldValidator): + def __init__(self, cnh: str) -> None: + self.cnh = cnh + + def validate(self) -> bool: + cnh = re.sub("[^0-9]", "", str(self.cnh)) + + if len(set(cnh)) == 1: + return False + + if len(cnh) != 11: + return False + + first_digit = self._validate_first_digit(cnh) + second_digit = self._validate_second_digit(cnh) + return cnh[9] == first_digit and cnh[10] == second_digit + + def _validate_first_digit(self, cnh: str) -> str: + self.dsc = 0 + sum = 0 + + for i in range(9, 0, -1): + sum += int(cnh[9 - i]) * i + + first_digit = sum % 11 + if first_digit >= 10: + first_digit, self.dsc = 0, 2 + return str(first_digit) + + def _validate_second_digit(self, cnh: str) -> str: + sum = 0 + + for i in range(1, 10): + sum += int(cnh[i - 1]) * i + + rest = sum % 11 + + second_digit = rest - self.dsc + if second_digit < 0: + second_digit += 11 + if second_digit >= 10: + second_digit = 0 + return str(second_digit) diff --git a/tests/test_cnh.py b/tests/test_cnh.py new file mode 100644 index 0000000..b41c8c4 --- /dev/null +++ b/tests/test_cnh.py @@ -0,0 +1,101 @@ +from string import ascii_letters + +import pytest +from pydantic import BaseModel, ValidationError + +from pydantic_br_validator import ( + CNH, + FieldDigitError, + FieldInvalidError, + FieldTypeError, +) + +cnh_mock = [ + "49761142867", + "15706519597", + "18820839790", + "93025633607", + "22255370700", + "74487688509", + "83002264521", + "21671642456", + "36407284795", + "93017746007", +] + + +@pytest.fixture +def person(): + class Person(BaseModel): + cnh: CNH + + yield Person + + +@pytest.mark.parametrize("cnh", cnh_mock) +def test_must_be_string(person, cnh): + p1 = person(cnh=cnh) + assert isinstance(p1.cnh, str) + + +@pytest.mark.parametrize("cnh", cnh_mock) +def test_must_accept_only_numbers(person, cnh): + p1 = person(cnh=cnh) + assert p1.cnh == cnh + + +@pytest.mark.parametrize("cnh", cnh_mock) +def test_must_fail_when_use_another_type(person, cnh): + with pytest.raises(ValidationError) as e: + person(cnh=int(cnh)) + assert FieldTypeError.msg_template in str(e.value) + + +@pytest.mark.parametrize("cnh", cnh_mock) +def test_must_fail_when_use_invalid_cnh(person, cnh): + with pytest.raises(ValidationError) as e: + invalid_cnh = cnh[:5] + cnh[6:] + person(cnh=invalid_cnh) + assert FieldInvalidError.msg_template in str(e.value) + + +@pytest.mark.parametrize("cnh", cnh_mock) +def test_must_fail_when_use_digits_count_above_cnh(person, cnh): + with pytest.raises(ValidationError) as e: + person(cnh=cnh * 2) + assert FieldInvalidError.msg_template in str(e.value) + + +@pytest.mark.parametrize("cnh", cnh_mock) +def test_must_fail_when_use_digits_count_below_cnh(person, cnh): + with pytest.raises(ValidationError) as e: + person(cnh=cnh[:5]) + assert FieldInvalidError.msg_template in str(e.value) + + +@pytest.mark.parametrize( + "cnh", + [ + "00000000000", + "11111111111", + "22222222222", + "33333333333", + "44444444444", + "55555555555", + "66666666666", + "77777777777", + "88888888888", + "99999999999", + ], +) +def test_must_fail_when_use_sequecial_digits(person, cnh): + with pytest.raises(ValidationError) as e: + person(cnh=cnh) + assert FieldInvalidError.msg_template in str(e.value) + + +def test_must_fail_when_not_use_only_digits(person): + with pytest.raises(ValidationError) as e: + letters = ascii_letters[:11] + person(cnh=letters) + assert FieldDigitError.msg_template in str(e.value)