File size: 10,257 Bytes
bfcc872
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
"""
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"])