"""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), [])