Spaces:
Sleeping
Sleeping
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 == ""
|