diff --git a/tests/unit/check_handling/__init__.py b/tests/unit/check_handling/__init__.py new file mode 100644 index 0000000..696923d --- /dev/null +++ b/tests/unit/check_handling/__init__.py @@ -0,0 +1 @@ +"""Unit tests for check_handling module.""" diff --git a/tests/unit/check_handling/test_regex_constants.py b/tests/unit/check_handling/test_regex_constants.py new file mode 100644 index 0000000..4a4bdbf --- /dev/null +++ b/tests/unit/check_handling/test_regex_constants.py @@ -0,0 +1,336 @@ +""" +Unit tests for regex_constants module. + +Tests all regex patterns defined in the check_handling.regex_constants module. +""" + +import re +import pytest +from corelibs.check_handling.regex_constants import ( + compile_re, + EMAIL_BASIC_REGEX, + DOMAIN_WITH_LOCALHOST_REGEX, + DOMAIN_WITH_LOCALHOST_PORT_REGEX, + DOMAIN_REGEX, +) + + +class TestCompileRe: + """Test cases for the compile_re function.""" + + def test_compile_re_returns_pattern(self) -> None: + """Test that compile_re returns a compiled regex Pattern object.""" + pattern = compile_re(r"test") + assert isinstance(pattern, re.Pattern) + + def test_compile_re_with_verbose_flag(self) -> None: + """Test that compile_re compiles with VERBOSE flag.""" + # Verbose mode allows whitespace and comments in regex + verbose_regex = r""" + \d+ # digits + \s+ # whitespace + """ + pattern = compile_re(verbose_regex) + assert pattern.match("123 ") + assert not pattern.match("abc") + + def test_compile_re_simple_pattern(self) -> None: + """Test compile_re with a simple pattern.""" + pattern = compile_re(r"^\d{3}$") + assert pattern.match("123") + assert not pattern.match("12") + assert not pattern.match("1234") + + +class TestEmailBasicRegex: + """Test cases for EMAIL_BASIC_REGEX pattern.""" + + @pytest.fixture + def email_pattern(self) -> re.Pattern[str]: + """Fixture that returns compiled email regex pattern.""" + return compile_re(EMAIL_BASIC_REGEX) + + @pytest.mark.parametrize("valid_email", [ + "user@example.com", + "test.user@example.com", + "user+tag@example.co.uk", + "first.last@subdomain.example.com", + "user123@test-domain.com", + "a@example.com", + "user_name@example.com", + "user-name@example.com", + "user@sub.domain.example.com", + "test!#$%&'*+-/=?^_`{|}~@example.com", + "1234567890@example.com", + "user@example-domain.com", + "user@domain.co", + # Regex allows these (even if not strictly RFC compliant): + "user.@example.com", # ends with dot before @ + "user..name@example.com", # consecutive dots in local part + ]) + def test_valid_emails( + self, email_pattern: re.Pattern[str], valid_email: str + ) -> None: + """Test that valid email addresses match the pattern.""" + assert email_pattern.match(valid_email), ( + f"Failed to match valid email: {valid_email}" + ) + + @pytest.mark.parametrize("invalid_email", [ + "", # empty string + "@example.com", # missing local part + "user@", # missing domain + "user", # no @ symbol + "user@.com", # domain starts with dot + "user@domain", # no TLD + "user @example.com", # space in local part + "user@exam ple.com", # space in domain + ".user@example.com", # starts with dot + "user@-example.com", # domain starts with hyphen + "user@example-.com", # domain part ends with hyphen + "user@example.c", # TLD too short (1 char) + "user@example.toolong", # TLD too long (>6 chars) + "user@@example.com", # double @ + "user@example@com", # multiple @ + "user@.example.com", # domain starts with dot + "user@example.com.", # ends with dot + "user@123.456.789.012", # numeric TLD not allowed + ]) + def test_invalid_emails( + self, email_pattern: re.Pattern[str], invalid_email: str + ) -> None: + """Test that invalid email addresses do not match the pattern.""" + assert not email_pattern.match(invalid_email), ( + f"Incorrectly matched invalid email: {invalid_email}" + ) + + def test_email_max_local_part_length( + self, email_pattern: re.Pattern[str] + ) -> None: + """Test email with maximum local part length (64 characters).""" + # Local part can be up to 64 chars (first char + 63 more) + local_part = "a" * 64 + email = f"{local_part}@example.com" + assert email_pattern.match(email) + + def test_email_exceeds_local_part_length( + self, email_pattern: re.Pattern[str] + ) -> None: + """Test email exceeding maximum local part length.""" + # 65 characters should not match + local_part = "a" * 65 + email = f"{local_part}@example.com" + assert not email_pattern.match(email) + + +class TestDomainWithLocalhostRegex: + """Test cases for DOMAIN_WITH_LOCALHOST_REGEX pattern.""" + + @pytest.fixture + def domain_localhost_pattern(self) -> re.Pattern[str]: + """Fixture that returns compiled domain with localhost regex pattern.""" + return compile_re(DOMAIN_WITH_LOCALHOST_REGEX) + + @pytest.mark.parametrize("valid_domain", [ + "localhost", + "example.com", + "subdomain.example.com", + "sub.domain.example.com", + "test-domain.com", + "example.co.uk", + "a.com", + "test123.example.com", + "my-site.example.org", + "multi.level.subdomain.example.com", + ]) + def test_valid_domains( + self, domain_localhost_pattern: re.Pattern[str], valid_domain: str + ) -> None: + """Test that valid domains (including localhost) match the pattern.""" + assert domain_localhost_pattern.match(valid_domain), ( + f"Failed to match valid domain: {valid_domain}" + ) + + @pytest.mark.parametrize("invalid_domain", [ + "", # empty string + "example", # no TLD + "-example.com", # starts with hyphen + "example-.com", # ends with hyphen + ".example.com", # starts with dot + "example.com.", # ends with dot + "example..com", # consecutive dots + "exam ple.com", # space in domain + "example.c", # TLD too short + "localhost:8080", # port not allowed in this pattern + "example.com:8080", # port not allowed in this pattern + "@example.com", # invalid character + "example@com", # invalid character + ]) + def test_invalid_domains( + self, domain_localhost_pattern: re.Pattern[str], invalid_domain: str + ) -> None: + """Test that invalid domains do not match the pattern.""" + assert not domain_localhost_pattern.match(invalid_domain), ( + f"Incorrectly matched invalid domain: {invalid_domain}" + ) + + +class TestDomainWithLocalhostPortRegex: + """Test cases for DOMAIN_WITH_LOCALHOST_PORT_REGEX pattern.""" + + @pytest.fixture + def domain_localhost_port_pattern(self) -> re.Pattern[str]: + """Fixture that returns compiled domain and localhost with port pattern.""" + return compile_re(DOMAIN_WITH_LOCALHOST_PORT_REGEX) + + @pytest.mark.parametrize("valid_domain", [ + "localhost", + "localhost:8080", + "localhost:3000", + "localhost:80", + "localhost:443", + "localhost:65535", + "example.com", + "example.com:8080", + "subdomain.example.com:3000", + "test-domain.com:443", + "example.co.uk", + "example.co.uk:8000", + "a.com:1", + "multi.level.subdomain.example.com:9999", + ]) + def test_valid_domains_with_port( + self, domain_localhost_port_pattern: re.Pattern[str], valid_domain: str + ) -> None: + """Test that valid domains with optional ports match the pattern.""" + assert domain_localhost_port_pattern.match(valid_domain), ( + f"Failed to match valid domain: {valid_domain}" + ) + + @pytest.mark.parametrize("invalid_domain", [ + "", # empty string + "example", # no TLD + "-example.com", # starts with hyphen + "example-.com", # ends with hyphen + ".example.com", # starts with dot + "example.com.", # ends with dot + "localhost:", # port without number + "example.com:", # port without number + "example.com:abc", # non-numeric port + "example.com: 8080", # space before port + "example.com:80 80", # space in port + "exam ple.com", # space in domain + "localhost :8080", # space before colon + ]) + def test_invalid_domains_with_port( + self, + domain_localhost_port_pattern: re.Pattern[str], + invalid_domain: str, + ) -> None: + """Test that invalid domains do not match the pattern.""" + assert not domain_localhost_port_pattern.match(invalid_domain), ( + f"Incorrectly matched invalid domain: {invalid_domain}" + ) + + def test_large_port_number( + self, domain_localhost_port_pattern: re.Pattern[str] + ) -> None: + """Test domain with large port numbers.""" + assert domain_localhost_port_pattern.match("example.com:65535") + # Regex doesn't validate port range + assert domain_localhost_port_pattern.match("example.com:99999") + + +class TestDomainRegex: + """Test cases for DOMAIN_REGEX pattern (no localhost).""" + + @pytest.fixture + def domain_pattern(self) -> re.Pattern[str]: + """Fixture that returns compiled domain regex pattern.""" + return compile_re(DOMAIN_REGEX) + + @pytest.mark.parametrize("valid_domain", [ + "example.com", + "subdomain.example.com", + "sub.domain.example.com", + "test-domain.com", + "example.co.uk", + "a.com", + "test123.example.com", + "my-site.example.org", + "multi.level.subdomain.example.com", + "example.co", + ]) + def test_valid_domains_no_localhost( + self, domain_pattern: re.Pattern[str], valid_domain: str + ) -> None: + """Test that valid domains match the pattern.""" + assert domain_pattern.match(valid_domain), ( + f"Failed to match valid domain: {valid_domain}" + ) + + @pytest.mark.parametrize("invalid_domain", [ + "", # empty string + "localhost", # localhost not allowed + "example", # no TLD + "-example.com", # starts with hyphen + "example-.com", # ends with hyphen + ".example.com", # starts with dot + "example.com.", # ends with dot + "example..com", # consecutive dots + "exam ple.com", # space in domain + "example.c", # TLD too short + "example.com:8080", # port not allowed + "@example.com", # invalid character + "example@com", # invalid character + ]) + def test_invalid_domains_no_localhost( + self, domain_pattern: re.Pattern[str], invalid_domain: str + ) -> None: + """Test that invalid domains do not match the pattern.""" + assert not domain_pattern.match(invalid_domain), ( + f"Incorrectly matched invalid domain: {invalid_domain}" + ) + + def test_localhost_not_allowed( + self, domain_pattern: re.Pattern[str] + ) -> None: + """Test that localhost is explicitly not allowed in DOMAIN_REGEX.""" + assert not domain_pattern.match("localhost") + + +class TestRegexPatternConsistency: + """Test cases for consistency across regex patterns.""" + + def test_all_patterns_compile(self) -> None: + """Test that all regex patterns can be compiled without errors.""" + patterns = [ + EMAIL_BASIC_REGEX, + DOMAIN_WITH_LOCALHOST_REGEX, + DOMAIN_WITH_LOCALHOST_PORT_REGEX, + DOMAIN_REGEX, + ] + for pattern in patterns: + compiled = compile_re(pattern) + assert isinstance(compiled, re.Pattern) + + def test_domain_patterns_are_strings(self) -> None: + """Test that all regex constants are strings.""" + assert isinstance(EMAIL_BASIC_REGEX, str) + assert isinstance(DOMAIN_WITH_LOCALHOST_REGEX, str) + assert isinstance(DOMAIN_WITH_LOCALHOST_PORT_REGEX, str) + assert isinstance(DOMAIN_REGEX, str) + + def test_domain_patterns_hierarchy(self) -> None: + """Test that domain patterns follow expected hierarchy.""" + # DOMAIN_WITH_LOCALHOST_PORT_REGEX should accept everything + # DOMAIN_WITH_LOCALHOST_REGEX accepts + domain_localhost = compile_re(DOMAIN_WITH_LOCALHOST_REGEX) + domain_localhost_port = compile_re(DOMAIN_WITH_LOCALHOST_PORT_REGEX) + + test_cases = ["example.com", "subdomain.example.com", "localhost"] + for test_case in test_cases: + if domain_localhost.match(test_case): + assert domain_localhost_port.match(test_case), ( + f"{test_case} should match both patterns" + )