# retoor from datetime import datetime, timezone import pytest from pydantic import ValidationError from devplacepy.config import QUIZ_MAX_OPTIONS, QUIZ_MAX_QUESTIONS from devplacepy.database import get_table from devplacepy.models import QuizDocument, QuizImportForm from devplacepy.services.quiz import store from devplacepy.services.quiz.store import QuizError from devplacepy.utils import generate_uid, make_combined_slug _counter = [0] DOCUMENT = { "title": "SQLite fundamentals", "description": "Questions on WAL and indexing.", "settings": {"shuffle_questions": True, "reveal_answers": True, "pass_percent": 70}, "questions": [ { "kind": "single_choice", "prompt": "Which journal mode allows concurrent readers?", "points": 2, "explanation": "WAL keeps readers off the writer's lock.", "options": [{"label": "DELETE"}, {"label": "WAL", "is_correct": True}], }, { "kind": "free_text", "prompt": "Why does a partial index help?", "points": 3, "expected_answer": "It keeps the live query off a one bucket index.", "grading_criteria": "Accept any answer mentioning the planner.", }, ], } def _user(prefix): _counter[0] += 1 uid = generate_uid() get_table("users").insert( { "uid": uid, "username": f"{prefix}{_counter[0]}", "email": f"{prefix}{_counter[0]}@t.dev", "role": "Member", "api_key": generate_uid(), "created_at": datetime.now(timezone.utc).isoformat(), } ) return get_table("users").find_one(uid=uid) def _quiz(owner, document): uid = generate_uid() get_table("quizzes").insert( store.born_live( { "uid": uid, "user_uid": owner["uid"], "slug": make_combined_slug(document.title, uid), "title": document.title, "description": document.description, "status": "draft", "published_at": "", "question_count": 0, "total_points": 0, "attempt_count": 0, "stars": 0, **store.document_settings(document), } ) ) store.import_questions(uid, document) return store.get_quiz(uid) def test_parse_document_accepts_the_reference_shape(): document = store.parse_document(DOCUMENT) assert document.title == "SQLite fundamentals" assert len(document.questions) == 2 def test_parse_document_rejects_junk(): with pytest.raises(QuizError): store.parse_document({"title": "x"}) def test_the_import_form_parses_a_json_string(): import json form = QuizImportForm(document=json.dumps(DOCUMENT)) assert form.document.title == "SQLite fundamentals" def test_the_import_form_rejects_invalid_json(): with pytest.raises(ValidationError): QuizImportForm(document="{not json") def test_a_document_needs_at_least_one_question(): with pytest.raises(ValidationError): QuizDocument(title="Empty quiz", questions=[]) def test_a_document_is_capped_at_the_question_limit(): question = {"kind": "numeric", "prompt": "q", "numeric_value": 1} with pytest.raises(ValidationError): QuizDocument(title="Too big", questions=[question] * (QUIZ_MAX_QUESTIONS + 1)) def test_a_question_is_capped_at_the_option_limit(): options = [{"label": f"L{index}"} for index in range(QUIZ_MAX_OPTIONS + 1)] options[0]["is_correct"] = True with pytest.raises(ValidationError): QuizDocument( title="Too many options", questions=[{"kind": "single_choice", "prompt": "q", "options": options}], ) def test_a_single_choice_needs_exactly_one_correct_option(): with pytest.raises(ValidationError): QuizDocument( title="Bad choice", questions=[ { "kind": "single_choice", "prompt": "q", "options": [{"label": "a"}, {"label": "b"}], } ], ) def test_a_free_text_needs_a_reference_or_criteria(): with pytest.raises(ValidationError): QuizDocument( title="Bad free text", questions=[{"kind": "free_text", "prompt": "explain"}], ) def test_a_matching_needs_a_right_hand_value_on_every_pair(): with pytest.raises(ValidationError): QuizDocument( title="Bad matching", questions=[ { "kind": "matching", "prompt": "q", "options": [{"label": "a", "match_value": "A"}, {"label": "b"}], } ], ) def test_document_settings_map_onto_the_quiz_columns(): settings = store.document_settings(store.parse_document(DOCUMENT)) assert settings["shuffle_questions"] == 1 assert settings["reveal_answers"] == 1 assert settings["pass_percent"] == 70 def test_importing_creates_every_question_in_order(local_db): owner = _user("qd") quiz = _quiz(owner, store.parse_document(DOCUMENT)) questions = store.list_questions(quiz["uid"]) assert [q["kind"] for q in questions] == ["single_choice", "free_text"] assert [q["position"] for q in questions] == [0, 1] def test_importing_recomputes_the_totals(local_db): owner = _user("qd") quiz = _quiz(owner, store.parse_document(DOCUMENT)) assert quiz["question_count"] == 2 assert quiz["total_points"] == 5 def test_importing_stores_the_kind_specific_fields(local_db): owner = _user("qd") quiz = _quiz(owner, store.parse_document(DOCUMENT)) free_text = store.list_questions(quiz["uid"])[1] assert free_text["expected_answer"].startswith("It keeps") assert free_text["grading_criteria"].startswith("Accept") def test_the_owner_export_round_trips_through_import(local_db): owner = _user("qd") quiz = _quiz(owner, store.parse_document(DOCUMENT)) exported = store.export_document(quiz["uid"], True) reimported = _quiz(owner, store.parse_document(exported)) assert reimported["question_count"] == quiz["question_count"] assert reimported["total_points"] == quiz["total_points"] assert store.export_document(reimported["uid"], True) == exported def test_the_owner_export_carries_the_answer_key(local_db): owner = _user("qd") quiz = _quiz(owner, store.parse_document(DOCUMENT)) exported = store.export_document(quiz["uid"], True) assert any(option.get("is_correct") for option in exported["questions"][0]["options"]) assert exported["questions"][1]["expected_answer"] def test_the_public_export_omits_the_answer_key(local_db): owner = _user("qd") quiz = _quiz(owner, store.parse_document(DOCUMENT)) exported = store.export_document(quiz["uid"], False) assert all("is_correct" not in option for option in exported["questions"][0]["options"]) assert "expected_answer" not in exported["questions"][1] assert "correct_boolean" not in exported["questions"][0] def test_the_public_export_keeps_the_prompts_and_labels(local_db): owner = _user("qd") quiz = _quiz(owner, store.parse_document(DOCUMENT)) exported = store.export_document(quiz["uid"], False) assert exported["questions"][0]["prompt"].startswith("Which journal mode") assert [option["label"] for option in exported["questions"][0]["options"]] == ["DELETE", "WAL"] def test_the_export_carries_the_settings(local_db): owner = _user("qd") quiz = _quiz(owner, store.parse_document(DOCUMENT)) exported = store.export_document(quiz["uid"], False) assert exported["settings"]["pass_percent"] == 70 assert exported["settings"]["reveal_answers"] is True def test_exporting_an_unknown_quiz_raises(local_db): with pytest.raises(QuizError): store.export_document("does-not-exist", True)