Spaces:
Running on Zero
Running on Zero
File size: 5,920 Bytes
66ee87e | 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 | """Request validation for /api/decide questions (app/validation.py)."""
import unittest
from app.validation import validate_questions
CHOICE = {"type": "choice", "instructions": "Which team?", "criteria": {"a": "billing", "b": "shipping"}}
class ValidateQuestionsTest(unittest.TestCase):
def assertRejects(self, questions, message, max_options=40):
with self.assertRaises(ValueError) as ctx:
validate_questions(questions, max_options=max_options)
self.assertEqual(str(ctx.exception), message)
def test_accepts_choice_score_and_noul(self):
validate_questions({
"team": CHOICE,
"urgency": {"type": "score", "instructions": "How urgent?", "criteria": ["Low", "High"]},
"angry": {"type": "noul", "instructions": "The customer sounds angry"},
}, max_options=40)
def test_type_defaults_to_choice(self):
q = {k: v for k, v in CHOICE.items() if k != "type"}
validate_questions({"team": q}, max_options=40)
def test_accepts_question_key_instead_of_instructions(self):
validate_questions({"angry": {"type": "noul", "question": "Angry?"}}, max_options=40)
def test_accepts_options_key_instead_of_criteria(self):
validate_questions({"t": {"type": "choice", "instructions": "x", "options": ["a", "b"]}}, max_options=40)
def test_rejects_empty_question_set(self):
self.assertRejects({}, "Add at least one question.")
def test_rejects_unknown_type(self):
self.assertRejects({"q": {"type": "rank", "instructions": "x"}},
"Question 'q': type must be choice, score or noul (got 'rank').")
def test_rejects_missing_instructions(self):
self.assertRejects({"q": {"type": "noul"}}, "Question 'q': add 'instructions' with the question text.")
def test_rejects_fewer_than_two_options(self):
self.assertRejects({"q": {"type": "choice", "instructions": "x", "criteria": {"a": "only"}}},
"Question 'q': give 'criteria' with at least 2 options.")
def test_rejects_non_collection_criteria(self):
self.assertRejects({"q": {"type": "score", "instructions": "x", "criteria": "low,high"}},
"Question 'q': give 'criteria' with at least 2 options.")
def test_accepts_exactly_max_options(self):
crit = {str(i): "o" for i in range(5)}
validate_questions({"q": {"type": "choice", "instructions": "x", "criteria": crit}}, max_options=5)
def test_rejects_more_than_max_options(self):
crit = {str(i): "o" for i in range(6)}
self.assertRejects({"q": {"type": "choice", "instructions": "x", "criteria": crit}},
"Question 'q': 6 options is over the lab's limit of 5.", max_options=5)
def test_noul_needs_no_criteria(self):
validate_questions({"q": {"type": "noul", "instructions": "x"}}, max_options=40)
if __name__ == "__main__":
unittest.main()
from app.validation import LIMITS, validate_models
class SizeLimitTest(unittest.TestCase):
def assertRejects(self, questions, message):
with self.assertRaises(ValueError) as ctx:
validate_questions(questions, max_options=40)
self.assertEqual(str(ctx.exception), message)
def test_too_many_questions(self):
qs = {f"q{i}": {"type": "noul", "instructions": "x"} for i in range(LIMITS["questions"] + 1)}
self.assertRejects(qs, f"At most {LIMITS['questions']} questions per request (got {LIMITS['questions'] + 1}).")
def test_question_name_too_long(self):
name = "n" * (LIMITS["name"] + 1)
self.assertRejects({name: {"type": "noul", "instructions": "x"}},
f"Question names are limited to {LIMITS['name']} characters.")
def test_instructions_too_long(self):
self.assertRejects({"q": {"type": "noul", "instructions": "x" * (LIMITS["instructions"] + 1)}},
f"Question 'q': instructions are limited to {LIMITS['instructions']} characters.")
def test_option_text_too_long(self):
crit = {"a": "x" * (LIMITS["option"] + 1), "b": "y"}
self.assertRejects({"q": {"type": "choice", "instructions": "x", "criteria": crit}},
f"Question 'q': each option (key and text) is limited to {LIMITS['option']} characters.")
def test_option_key_too_long(self):
crit = {"k" * (LIMITS["option"] + 1): "x", "b": "y"}
self.assertRejects({"q": {"type": "choice", "instructions": "x", "criteria": crit}},
f"Question 'q': each option (key and text) is limited to {LIMITS['option']} characters.")
def test_non_text_option_is_rejected(self):
self.assertRejects({"q": {"type": "choice", "instructions": "x", "criteria": {"a": {"nested": 1}, "b": "y"}}},
"Question 'q': options must be text.")
def test_limits_at_the_boundary_are_accepted(self):
crit = {"a": "x" * LIMITS["option"], "b": "y"}
validate_questions({"n" * LIMITS["name"]: {"type": "choice", "instructions": "i" * LIMITS["instructions"],
"criteria": crit}}, max_options=40)
class ValidateModelsTest(unittest.TestCase):
KNOWN = ["a", "b", "laya"]
def test_none_means_every_model(self):
self.assertEqual(validate_models(None, self.KNOWN), self.KNOWN)
def test_duplicates_are_dropped_keeping_order(self):
self.assertEqual(validate_models(["b", "a", "b"], self.KNOWN), ["b", "a"])
def test_unknown_key_is_rejected_by_name(self):
with self.assertRaises(ValueError) as ctx:
validate_models(["a", "zzz"], self.KNOWN)
self.assertEqual(str(ctx.exception), "Unknown model 'zzz'. Known models: a, b, laya.")
def test_empty_list_is_allowed(self):
self.assertEqual(validate_models([], self.KNOWN), [])
|