from pathlib import Path
import pytest
from fastapi import HTTPException
from app.schemas import AdminSpeakingMockTestBuilderRequest
from app.services.admin_content_service import AdminContentService
from app.services.mock_data_service import MockDataService
@pytest.fixture()
def speaking_service(tmp_path, monkeypatch):
MockDataService._singleton = MockDataService(tmp_path)
monkeypatch.setenv("MOCK_DATA_ROOT", str(tmp_path))
return AdminContentService(data_root=tmp_path)
def speaking_builder(**overrides):
data = {
"title": "Admin Speaking Test",
"book_code": "SP-ADMIN",
"status": "published",
"time": 13,
"thumbnail": "thumb-id",
"parts": [
{
"title": "Speaking Part 1",
"time": 5,
"instruction_html": "
Part 1 instruction
",
"questions": [
{"title": "Do you work or study?", "time_to_think": 0, "time_limit": 30, "audio_url": ""}
],
},
{
"title": "Speaking Part 2",
"time": 3,
"instruction_html": "Part 2 instruction
",
"questions": [
{
"title": "Describe a useful object",
"description": "",
"time_to_think": 60,
"time_limit": 120,
"audio_url": "https://example.com/q.mp3",
}
],
},
{
"title": "Speaking Part 3",
"time": 5,
"instruction_html": "Part 3 instruction
",
"questions": [
{"title": "Why do people keep old things?", "time_to_think": 0, "time_limit": 45, "audio_url": ""}
],
},
],
}
data.update(overrides)
return AdminSpeakingMockTestBuilderRequest(**data)
def test_create_speaking_builder_writes_mock_and_quizzes(speaking_service):
result = speaking_service.save_speaking_mock_test_builder(speaking_builder())
assert result.mock_test["skill_id"] == 8
assert result.full_quiz["type"] == 8
assert result.full_quiz["skill_id"] == 8
assert len(result.full_quiz["parts"]) == 3
assert result.mock_test["quizzes"]["full"]["question_count"] == 3
assert set(result.mock_test["quizzes"]) == {"full", "part_1", "part_2", "part_2&3", "part_3"}
assert result.full_quiz["parts"][1]["questions"][0]["description"] == ""
assert result.full_quiz["parts"][1]["questions"][0]["time_to_think"] == 60
assert result.full_quiz["parts"][1]["question_sets"][0]["question_type"] == "SPEAKING"
folder = Path(speaking_service._data_root) / "admin_generated" / "speaking" / str(result.mock_test_id)
assert (folder / f"mock_test_{result.mock_test_id}.json").exists()
assert (folder / f"full_{result.full_quiz_id}.json").exists()
assert (folder / f"part_2&3_{result.part_quiz_ids[2]}.json").exists()
def test_get_speaking_builder_round_trips_from_raw_json(speaking_service):
created = speaking_service.save_speaking_mock_test_builder(speaking_builder())
loaded = speaking_service.get_speaking_mock_test_builder(created.mock_test_id)
assert loaded.builder["title"] == "Admin Speaking Test"
assert loaded.builder["parts"][0]["questions"][0]["title"] == "Do you work or study?"
assert loaded.builder["parts"][1]["questions"][0]["audio_url"] == "https://example.com/q.mp3"
assert loaded.full_quiz_id == created.full_quiz_id
def test_update_speaking_builder_keeps_ids_and_creates_backup(speaking_service):
created = speaking_service.save_speaking_mock_test_builder(speaking_builder())
first_question_id = created.full_quiz["parts"][0]["questions"][0]["id"]
updated_builder = speaking_builder(title="Updated Speaking Test")
updated_builder.parts[0].questions[0].title = "Are you a student?"
updated = speaking_service.save_speaking_mock_test_builder(updated_builder, mock_test_id=created.mock_test_id)
assert updated.mock_test_id == created.mock_test_id
assert updated.full_quiz_id == created.full_quiz_id
assert updated.full_quiz["parts"][0]["questions"][0]["id"] == first_question_id
assert updated.full_quiz["parts"][0]["questions"][0]["title"] == "Are you a student?"
assert updated.backup_paths
def test_validate_speaking_builder_rejects_missing_questions(speaking_service):
builder = speaking_builder()
builder.parts[2].questions = []
with pytest.raises(HTTPException) as exc:
speaking_service.save_speaking_mock_test_builder(builder)
assert exc.value.status_code == 400
assert "Part 3 needs at least one question" in exc.value.detail