Spaces:
Sleeping
Sleeping
| """ | |
| Unit Tests for Validation Module | |
| Tests for URL validation, query validation, and input sanitization. | |
| """ | |
| from __future__ import annotations | |
| import pytest | |
| from src.validation import ( | |
| validate_url, | |
| validate_medium_url, | |
| validate_batch_urls, | |
| validate_search_query, | |
| validate_tag, | |
| validate_positive_int, | |
| validate_post_id, | |
| ValidationResult, | |
| ) | |
| class TestURLValidation: | |
| """Tests for URL validation.""" | |
| def test_valid_http_url(self) -> None: | |
| """Valid HTTP URL should pass.""" | |
| result = validate_url("http://example.com/article") | |
| assert result.is_valid | |
| assert result.value == "http://example.com/article" | |
| def test_valid_https_url(self) -> None: | |
| """Valid HTTPS URL should pass.""" | |
| result = validate_url("https://medium.com/@user/article") | |
| assert result.is_valid | |
| def test_empty_url_fails(self) -> None: | |
| """Empty URL should fail.""" | |
| result = validate_url("") | |
| assert not result.is_valid | |
| assert "required" in result.error.lower() | |
| def test_javascript_url_blocked(self) -> None: | |
| """JavaScript URLs should be blocked.""" | |
| result = validate_url("javascript:alert('xss')") | |
| assert not result.is_valid | |
| assert "dangerous" in result.error.lower() | |
| def test_data_url_blocked(self) -> None: | |
| """Data URLs should be blocked.""" | |
| result = validate_url("data:text/html,<script>alert('xss')</script>") | |
| assert not result.is_valid | |
| def test_too_long_url_fails(self) -> None: | |
| """URLs over max length should fail.""" | |
| long_url = "https://example.com/" + "a" * 3000 | |
| result = validate_url(long_url) | |
| assert not result.is_valid | |
| assert "length" in result.error.lower() | |
| def test_invalid_scheme_fails(self) -> None: | |
| """Invalid schemes should fail.""" | |
| result = validate_url("ftp://example.com/file") | |
| assert not result.is_valid | |
| assert "http" in result.error.lower() | |
| def test_url_with_whitespace_trimmed(self) -> None: | |
| """Whitespace should be trimmed.""" | |
| result = validate_url(" https://medium.com/article ") | |
| assert result.is_valid | |
| assert result.value == "https://medium.com/article" | |
| class TestMediumURLValidation: | |
| """Tests for Medium-specific URL validation.""" | |
| def test_valid_medium_url(self) -> None: | |
| """Medium.com URLs should pass.""" | |
| result = validate_medium_url("https://medium.com/@user/article") | |
| assert result.is_valid | |
| def test_valid_publication_url(self) -> None: | |
| """Publication URLs should pass.""" | |
| result = validate_medium_url("https://towardsdatascience.com/article") | |
| assert result.is_valid | |
| def test_non_medium_url_fails(self) -> None: | |
| """Non-Medium URLs should fail.""" | |
| result = validate_medium_url("https://google.com/search") | |
| assert not result.is_valid | |
| assert "Medium" in result.error | |
| class TestBatchURLValidation: | |
| """Tests for batch URL validation.""" | |
| def test_valid_batch(self) -> None: | |
| """Valid batch should return valid URLs.""" | |
| urls = [ | |
| "https://medium.com/article1", | |
| "https://medium.com/article2", | |
| ] | |
| valid, errors = validate_batch_urls(urls) | |
| assert len(valid) == 2 | |
| assert len(errors) == 0 | |
| def test_mixed_batch(self) -> None: | |
| """Mixed batch should separate valid and invalid.""" | |
| urls = [ | |
| "https://medium.com/article1", | |
| "invalid-url", | |
| "https://medium.com/article2", | |
| ] | |
| valid, errors = validate_batch_urls(urls) | |
| assert len(valid) == 2 | |
| assert len(errors) == 1 | |
| def test_empty_batch(self) -> None: | |
| """Empty batch should return error.""" | |
| valid, errors = validate_batch_urls([]) | |
| assert len(valid) == 0 | |
| assert len(errors) == 1 | |
| def test_batch_size_limit(self) -> None: | |
| """Batch over limit should fail.""" | |
| urls = [f"https://medium.com/article{i}" for i in range(25)] | |
| valid, errors = validate_batch_urls(urls) | |
| assert len(valid) == 0 | |
| assert "maximum" in errors[0]["error"].lower() | |
| class TestSearchQueryValidation: | |
| """Tests for search query validation.""" | |
| def test_valid_query(self) -> None: | |
| """Valid query should pass.""" | |
| result = validate_search_query("python async programming") | |
| assert result.is_valid | |
| assert result.value == "python async programming" | |
| def test_empty_query_fails(self) -> None: | |
| """Empty query should fail.""" | |
| result = validate_search_query("") | |
| assert not result.is_valid | |
| def test_short_query_fails(self) -> None: | |
| """Query under 2 chars should fail.""" | |
| result = validate_search_query("a") | |
| assert not result.is_valid | |
| def test_query_sanitization(self) -> None: | |
| """Dangerous characters should be removed.""" | |
| result = validate_search_query("<script>evil</script>") | |
| assert result.is_valid | |
| assert "<" not in result.value | |
| assert ">" not in result.value | |
| assert result.sanitized | |
| class TestTagValidation: | |
| """Tests for tag validation.""" | |
| def test_valid_tag(self) -> None: | |
| """Valid tag should pass.""" | |
| result = validate_tag("python") | |
| assert result.is_valid | |
| assert result.value == "python" | |
| def test_tag_with_hyphen(self) -> None: | |
| """Tags with hyphens should pass.""" | |
| result = validate_tag("machine-learning") | |
| assert result.is_valid | |
| def test_tag_sanitization(self) -> None: | |
| """Tags should be sanitized.""" | |
| result = validate_tag("Machine Learning") | |
| assert result.is_valid | |
| assert result.value == "machine-learning" | |
| def test_empty_tag_fails(self) -> None: | |
| """Empty tag should fail.""" | |
| result = validate_tag("") | |
| assert not result.is_valid | |
| class TestNumericValidation: | |
| """Tests for numeric validation.""" | |
| def test_valid_int(self) -> None: | |
| """Valid integer should pass.""" | |
| result = validate_positive_int(5, "count", 1, 10) | |
| assert result.is_valid | |
| assert result.value == "5" | |
| def test_below_min_fails(self) -> None: | |
| """Value below min should fail.""" | |
| result = validate_positive_int(0, "count", 1, 10) | |
| assert not result.is_valid | |
| def test_above_max_fails(self) -> None: | |
| """Value above max should fail.""" | |
| result = validate_positive_int(100, "count", 1, 10) | |
| assert not result.is_valid | |
| def test_non_int_fails(self) -> None: | |
| """Non-integer should fail.""" | |
| result = validate_positive_int("abc", "count") | |
| assert not result.is_valid | |
| class TestPostIDValidation: | |
| """Tests for post ID validation.""" | |
| def test_valid_post_id(self) -> None: | |
| """Valid hex post ID should pass.""" | |
| result = validate_post_id("abc123def456") | |
| assert result.is_valid | |
| def test_short_post_id_fails(self) -> None: | |
| """Short post ID should fail.""" | |
| result = validate_post_id("abc") | |
| assert not result.is_valid | |
| def test_non_hex_fails(self) -> None: | |
| """Non-hex characters should fail.""" | |
| result = validate_post_id("xyz123ghijkl") | |
| assert not result.is_valid | |
| def test_empty_post_id_fails(self) -> None: | |
| """Empty post ID should fail.""" | |
| result = validate_post_id("") | |
| assert not result.is_valid | |