Spaces:
Sleeping
Sleeping
| """ | |
| Tests for src/analyzer/utils/validation.py | |
| Tests cover: | |
| - validate_grant_id(): grant ID validation and normalization | |
| - validate_url(): URL validation with allowlist support | |
| - sanitize_filename(): safe filename generation | |
| - validate_search_query(): search query validation | |
| - validate_positive_int(): positive integer validation | |
| - validate_date_string(): ISO date validation | |
| """ | |
| import pytest | |
| from src.analyzer.utils.validation import ( | |
| validate_grant_id, | |
| validate_url, | |
| sanitize_filename, | |
| validate_search_query, | |
| validate_positive_int, | |
| validate_date_string, | |
| ) | |
| from src.analyzer.utils.errors import ValidationError | |
| class TestValidateGrantId: | |
| """Test validate_grant_id() for grant ID validation.""" | |
| def test_validate_grant_id_numeric(self): | |
| assert validate_grant_id("2315") == "2315" | |
| def test_validate_grant_id_with_prefix(self): | |
| assert validate_grant_id("competition-2315") == "2315" | |
| def test_validate_grant_id_with_short_prefix(self): | |
| assert validate_grant_id("comp-2315") == "2315" | |
| def test_validate_grant_id_with_grant_prefix(self): | |
| assert validate_grant_id("grant-2315") == "2315" | |
| def test_validate_grant_id_case_insensitive(self): | |
| assert validate_grant_id("COMPETITION-2315") == "2315" | |
| assert validate_grant_id("Competition-2315") == "2315" | |
| def test_validate_grant_id_with_whitespace(self): | |
| assert validate_grant_id(" 2315 ") == "2315" | |
| assert validate_grant_id(" competition-2315 ") == "2315" | |
| def test_validate_grant_id_empty(self): | |
| with pytest.raises(ValidationError, match="cannot be empty"): | |
| validate_grant_id("") | |
| def test_validate_grant_id_invalid_format(self): | |
| with pytest.raises(ValidationError, match="Invalid grant ID format"): | |
| validate_grant_id("invalid") | |
| def test_validate_grant_id_too_short(self): | |
| # Minimum 3 digits required | |
| with pytest.raises(ValidationError, match="Invalid grant ID format"): | |
| validate_grant_id("12") | |
| def test_validate_grant_id_too_long(self): | |
| # Maximum 7 digits | |
| with pytest.raises(ValidationError, match="Invalid grant ID format"): | |
| validate_grant_id("12345678") | |
| def test_validate_grant_id_valid_lengths(self): | |
| # 3-7 digits should all work | |
| assert validate_grant_id("123") == "123" | |
| assert validate_grant_id("1234567") == "1234567" | |
| class TestValidateUrl: | |
| """Test validate_url() for URL validation.""" | |
| def test_validate_url_https(self): | |
| url = "https://example.com/path" | |
| assert validate_url(url) == url | |
| def test_validate_url_http(self): | |
| url = "http://example.com/path" | |
| assert validate_url(url) == url | |
| def test_validate_url_with_query(self): | |
| url = "https://example.com/path?key=value" | |
| assert validate_url(url) == url | |
| def test_validate_url_empty(self): | |
| with pytest.raises(ValidationError, match="cannot be empty"): | |
| validate_url("") | |
| def test_validate_url_no_protocol(self): | |
| with pytest.raises(ValidationError, match="must start with http"): | |
| validate_url("example.com") | |
| def test_validate_url_invalid_protocol(self): | |
| with pytest.raises(ValidationError, match="must start with http"): | |
| validate_url("ftp://example.com") | |
| def test_validate_url_no_hostname(self): | |
| with pytest.raises(ValidationError, match="has no hostname"): | |
| validate_url("https://") | |
| def test_validate_url_with_allowlist(self): | |
| url = "https://example.com/path" | |
| allowed = {"example.com", "test.com"} | |
| assert validate_url(url, allowed_hosts=allowed) == url | |
| def test_validate_url_not_in_allowlist(self): | |
| url = "https://evil.com/path" | |
| allowed = {"example.com", "test.com"} | |
| with pytest.raises(ValidationError, match="not in allowlist"): | |
| validate_url(url, allowed_hosts=allowed) | |
| def test_validate_url_strips_whitespace(self): | |
| url = " https://example.com " | |
| assert validate_url(url) == "https://example.com" | |
| class TestSanitizeFilename: | |
| """Test sanitize_filename() for safe filename generation.""" | |
| def test_sanitize_filename_safe(self): | |
| assert sanitize_filename("report.pdf") == "report.pdf" | |
| def test_sanitize_filename_with_spaces(self): | |
| result = sanitize_filename("my report.pdf") | |
| assert result == "my_report.pdf" | |
| def test_sanitize_filename_path_traversal(self): | |
| result = sanitize_filename("../../../etc/passwd") | |
| assert result == "etc_passwd" | |
| assert ".." not in result | |
| assert "/" not in result | |
| def test_sanitize_filename_special_chars(self): | |
| result = sanitize_filename("file@#$%.txt") | |
| assert result == "file____.txt" | |
| def test_sanitize_filename_max_length(self): | |
| long_name = "a" * 300 | |
| result = sanitize_filename(long_name, max_length=50) | |
| assert len(result) == 50 | |
| def test_sanitize_filename_empty(self): | |
| with pytest.raises(ValidationError, match="cannot be empty"): | |
| sanitize_filename("") | |
| def test_sanitize_filename_only_special_chars(self): | |
| with pytest.raises(ValidationError, match="empty result"): | |
| sanitize_filename("@#$%") | |
| def test_sanitize_filename_strips_dots(self): | |
| result = sanitize_filename("...file.txt...") | |
| assert result == "file.txt" | |
| class TestValidateSearchQuery: | |
| """Test validate_search_query() for search query validation.""" | |
| def test_validate_search_query_normal(self): | |
| query = "machine learning" | |
| assert validate_search_query(query) == query | |
| def test_validate_search_query_strips(self): | |
| query = " machine learning " | |
| assert validate_search_query(query) == "machine learning" | |
| def test_validate_search_query_empty(self): | |
| with pytest.raises(ValidationError, match="cannot be empty"): | |
| validate_search_query("") | |
| def test_validate_search_query_whitespace_only(self): | |
| with pytest.raises(ValidationError, match="cannot be whitespace"): | |
| validate_search_query(" ") | |
| def test_validate_search_query_too_long(self): | |
| query = "a" * 600 | |
| with pytest.raises(ValidationError, match="too long"): | |
| validate_search_query(query, max_length=500) | |
| def test_validate_search_query_at_max_length(self): | |
| query = "a" * 500 | |
| result = validate_search_query(query, max_length=500) | |
| assert len(result) == 500 | |
| def test_validate_search_query_with_special_chars(self): | |
| query = "machine learning & AI (2024)" | |
| assert validate_search_query(query) == query | |
| class TestValidatePositiveInt: | |
| """Test validate_positive_int() for positive integer validation.""" | |
| def test_validate_positive_int_valid(self): | |
| assert validate_positive_int(5) == 5 | |
| assert validate_positive_int("10") == 10 | |
| def test_validate_positive_int_zero(self): | |
| with pytest.raises(ValidationError, match="must be positive"): | |
| validate_positive_int(0) | |
| def test_validate_positive_int_negative(self): | |
| with pytest.raises(ValidationError, match="must be positive"): | |
| validate_positive_int(-5) | |
| def test_validate_positive_int_float(self): | |
| # Should convert to int | |
| assert validate_positive_int(5.9) == 5 | |
| def test_validate_positive_int_string(self): | |
| assert validate_positive_int("42") == 42 | |
| def test_validate_positive_int_invalid_type(self): | |
| with pytest.raises(ValidationError, match="must be an integer"): | |
| validate_positive_int("not a number") | |
| def test_validate_positive_int_custom_name(self): | |
| with pytest.raises(ValidationError, match="limit must be positive"): | |
| validate_positive_int(-1, name="limit") | |
| class TestValidateDateString: | |
| """Test validate_date_string() for ISO date validation.""" | |
| def test_validate_date_string_valid(self): | |
| date = "2024-12-31" | |
| assert validate_date_string(date) == date | |
| def test_validate_date_string_strips(self): | |
| date = " 2024-12-31 " | |
| assert validate_date_string(date) == "2024-12-31" | |
| def test_validate_date_string_empty(self): | |
| with pytest.raises(ValidationError, match="cannot be empty"): | |
| validate_date_string("") | |
| def test_validate_date_string_wrong_format(self): | |
| with pytest.raises(ValidationError, match="must be in YYYY-MM-DD format"): | |
| validate_date_string("31/12/2024") | |
| def test_validate_date_string_invalid_month(self): | |
| with pytest.raises(ValidationError, match="Invalid date"): | |
| validate_date_string("2024-13-01") | |
| def test_validate_date_string_invalid_day(self): | |
| with pytest.raises(ValidationError, match="Invalid date"): | |
| validate_date_string("2024-12-32") | |
| def test_validate_date_string_leap_year(self): | |
| # 2024 is a leap year | |
| assert validate_date_string("2024-02-29") == "2024-02-29" | |
| def test_validate_date_string_non_leap_year(self): | |
| # 2023 is not a leap year | |
| with pytest.raises(ValidationError, match="Invalid date"): | |
| validate_date_string("2023-02-29") | |
| def test_validate_date_string_custom_name(self): | |
| with pytest.raises(ValidationError, match="deadline cannot be empty"): | |
| validate_date_string("", name="deadline") | |
| class TestIntegration: | |
| """Test validation functions work together correctly.""" | |
| def test_grant_workflow(self): | |
| # Simulate validating grant-related inputs | |
| grant_id = validate_grant_id("competition-2315") | |
| assert grant_id == "2315" | |
| url = validate_url("https://apply.innovateuk.org/competition/2315") | |
| assert "apply.innovateuk.org" in url | |
| deadline = validate_date_string("2024-12-31") | |
| assert deadline == "2024-12-31" | |
| def test_search_workflow(self): | |
| # Simulate validating search inputs | |
| query = validate_search_query(" AI funding ") | |
| assert query == "AI funding" | |
| limit = validate_positive_int("10", name="limit") | |
| assert limit == 10 | |
| if __name__ == "__main__": | |
| pytest.main([__file__, "-v"]) | |