File size: 8,148 Bytes
f9e47e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Tests for sunbird-ai-app/backend/validators.py

Covers:
  - Task 2.6: Property-based tests using Hypothesis (Properties 1, 2, 3)
  - Task 2.7: Edge-case unit tests
"""

from __future__ import annotations

import sys
import os

# Ensure the sunbird-ai-app package root is on the path so that
# `from backend.validators import ...` resolves correctly regardless of
# where pytest is invoked from.
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

import pytest
from hypothesis import given, settings, assume
from hypothesis import strategies as st

from backend.validators import (
    ACCEPTED_AUDIO_FORMATS,
    SUPPORTED_LANGUAGES,
    validate_audio_duration,
    validate_audio_format,
    validate_language_selection,
    validate_text_input,
)

# ---------------------------------------------------------------------------
# Task 2.6 — Property-based tests (Hypothesis)
# ---------------------------------------------------------------------------

# Feature: sunbird-ai-app, Property 1: Text input validation accepts any non-whitespace text and rejects whitespace-only strings

@given(st.text().filter(lambda s: s.strip() == ""))
@settings(max_examples=100)
def test_property1_whitespace_only_rejected(text: str) -> None:
    """
    **Validates: Requirements 2.1, 2.2**

    For any string composed entirely of whitespace (including the empty
    string), validate_text_input SHALL return (False, <non-empty error msg>).
    """
    # Feature: sunbird-ai-app, Property 1: Text input validation accepts any non-whitespace text and rejects whitespace-only strings
    valid, msg = validate_text_input(text)
    assert valid is False
    assert isinstance(msg, str) and len(msg) > 0


@given(st.text(min_size=1).filter(lambda s: s.strip() != ""))
@settings(max_examples=100)
def test_property1_non_whitespace_accepted(text: str) -> None:
    """
    **Validates: Requirements 2.1, 2.2**

    For any string containing at least one non-whitespace character,
    validate_text_input SHALL return (True, "").
    """
    # Feature: sunbird-ai-app, Property 1: Text input validation accepts any non-whitespace text and rejects whitespace-only strings
    valid, msg = validate_text_input(text)
    assert valid is True
    assert msg == ""


# Feature: sunbird-ai-app, Property 2: Audio format validation accepts exactly the supported formats and rejects all others

@given(
    base=st.text(min_size=1).filter(lambda s: "." not in s),
    ext=st.sampled_from(sorted(ACCEPTED_AUDIO_FORMATS)),
)
@settings(max_examples=100)
def test_property2_accepted_formats_lowercase(base: str, ext: str) -> None:
    """
    **Validates: Requirements 3.1, 3.5**

    For any filename whose extension (lower-case) is in the accepted set,
    validate_audio_format SHALL return (True, "").
    """
    # Feature: sunbird-ai-app, Property 2: Audio format validation accepts exactly the supported formats and rejects all others
    filename = f"{base}.{ext}"
    valid, msg = validate_audio_format(filename)
    assert valid is True
    assert msg == ""


@given(
    base=st.text(min_size=1).filter(lambda s: "." not in s),
    ext=st.sampled_from(sorted(ACCEPTED_AUDIO_FORMATS)),
)
@settings(max_examples=100)
def test_property2_accepted_formats_uppercase(base: str, ext: str) -> None:
    """
    **Validates: Requirements 3.1, 3.5**

    For any filename whose extension (upper-case) is in the accepted set,
    validate_audio_format SHALL return (True, "") — case-insensitive check.
    """
    # Feature: sunbird-ai-app, Property 2: Audio format validation accepts exactly the supported formats and rejects all others
    filename = f"{base}.{ext.upper()}"
    valid, msg = validate_audio_format(filename)
    assert valid is True
    assert msg == ""


@given(
    base=st.text(min_size=1).filter(lambda s: "." not in s),
    ext=st.text(min_size=1).filter(
        lambda s: s.lower() not in ACCEPTED_AUDIO_FORMATS and "." not in s
    ),
)
@settings(max_examples=100)
def test_property2_unsupported_formats_rejected(base: str, ext: str) -> None:
    """
    **Validates: Requirements 3.1, 3.5**

    For any filename whose extension is NOT in the accepted set,
    validate_audio_format SHALL return (False, <non-empty error msg>).
    """
    # Feature: sunbird-ai-app, Property 2: Audio format validation accepts exactly the supported formats and rejects all others
    filename = f"{base}.{ext}"
    valid, msg = validate_audio_format(filename)
    assert valid is False
    assert isinstance(msg, str) and len(msg) > 0


# Feature: sunbird-ai-app, Property 3: Audio duration validation accepts files ≤ 5 minutes and rejects files > 5 minutes

@given(st.floats(min_value=0.01, max_value=300.0, allow_nan=False, allow_infinity=False))
@settings(max_examples=100)
def test_property3_valid_duration_accepted(duration: float) -> None:
    """
    **Validates: Requirements 3.2, 3.3**

    For any duration d where 0 < d <= 300, validate_audio_duration(d)
    SHALL return (True, "").
    """
    # Feature: sunbird-ai-app, Property 3: Audio duration validation accepts files ≤ 5 minutes and rejects files > 5 minutes
    valid, msg = validate_audio_duration(duration)
    assert valid is True
    assert msg == ""


@given(st.floats(min_value=300.01, max_value=1e9, allow_nan=False, allow_infinity=False))
@settings(max_examples=100)
def test_property3_excessive_duration_rejected(duration: float) -> None:
    """
    **Validates: Requirements 3.2, 3.3**

    For any duration d > 300, validate_audio_duration(d)
    SHALL return (False, <non-empty error msg>).
    """
    # Feature: sunbird-ai-app, Property 3: Audio duration validation accepts files ≤ 5 minutes and rejects files > 5 minutes
    valid, msg = validate_audio_duration(duration)
    assert valid is False
    assert isinstance(msg, str) and len(msg) > 0


# ---------------------------------------------------------------------------
# Task 2.7 — Edge-case unit tests
# ---------------------------------------------------------------------------


class TestValidateTextInput:
    def test_empty_string_rejected(self) -> None:
        valid, msg = validate_text_input("")
        assert valid is False
        assert len(msg) > 0

    def test_whitespace_only_rejected(self) -> None:
        valid, msg = validate_text_input("   ")
        assert valid is False
        assert len(msg) > 0

    def test_valid_text_accepted(self) -> None:
        valid, msg = validate_text_input("hello")
        assert valid is True
        assert msg == ""


class TestValidateLanguageSelection:
    def test_none_rejected(self) -> None:
        valid, msg = validate_language_selection(None)
        assert valid is False
        assert len(msg) > 0

    def test_unsupported_language_rejected(self) -> None:
        valid, msg = validate_language_selection("French")
        assert valid is False
        assert len(msg) > 0

    def test_supported_language_accepted(self) -> None:
        valid, msg = validate_language_selection("Luganda")
        assert valid is True
        assert msg == ""


class TestValidateAudioDuration:
    def test_boundary_300_accepted(self) -> None:
        """Exactly 300 s is at the limit and must be accepted."""
        valid, msg = validate_audio_duration(300.0)
        assert valid is True
        assert msg == ""

    def test_just_above_300_rejected(self) -> None:
        """300.001 s is just over the limit and must be rejected."""
        valid, msg = validate_audio_duration(300.001)
        assert valid is False
        assert len(msg) > 0


class TestValidateAudioFormat:
    def test_unsupported_format_rejected(self) -> None:
        valid, msg = validate_audio_format("audio.txt")
        assert valid is False
        assert len(msg) > 0

    def test_uppercase_extension_accepted(self) -> None:
        """Extension matching must be case-insensitive."""
        valid, msg = validate_audio_format("audio.MP3")
        assert valid is True
        assert msg == ""

    def test_wav_accepted(self) -> None:
        valid, msg = validate_audio_format("audio.wav")
        assert valid is True
        assert msg == ""