lct-hack/backend/tests/test_materials.py

168 lines
6.2 KiB
Python
Raw Normal View History

"""Учебные материалы: создание, назначение, изучение и безопасная загрузка."""
import base64
from uuid import UUID
import pytest
from fastapi.testclient import TestClient
from app.config import get_settings
from app.main import app
from app.session.hub import hub
@pytest.fixture
def client(monkeypatch):
monkeypatch.setenv("DEMO_NO_DB", "true")
monkeypatch.setenv("DEV_AUTH_BYPASS", "true")
get_settings.cache_clear()
try:
with TestClient(app) as test_client:
yield test_client
finally:
get_settings.cache_clear()
def _instructor(client: TestClient) -> None:
response = client.post("/api/auth/dev-token")
assert response.status_code == 200
def _trainee(client: TestClient) -> None:
client.post("/api/auth/logout")
response = client.post(
"/api/auth/login", json={"login": "demo-trainee", "password": "demo"}
)
assert response.status_code == 200
def test_teacher_creates_assigns_and_trainee_completes_text_material(client):
_instructor(client)
created = client.post("/api/materials", json={
"title": "Порядок доклада старшему",
"description": "Перед практическим занятием",
"level": "L2",
"kind": "text",
"body": "Передайте адрес, тип события, задачу и подтвердите выезд.",
"scenario_id": "fire-apartment-l2",
})
assert created.status_code == 201, created.text
material_id = created.json()["id"]
assert created.json()["assignment_count"] == 0
assigned = client.put(
f"/api/materials/{material_id}/assign/00000000-0000-4000-8000-000000000112"
)
assert assigned.status_code == 200, assigned.text
assert assigned.json()["assigned_at"]
_trainee(client)
listing = client.get("/api/materials")
assert listing.status_code == 200
item = next(item for item in listing.json() if item["id"] == material_id)
assert item["body"].startswith("Передайте адрес")
assert item["completed_at"] is None
completed = client.post(f"/api/materials/{material_id}/complete")
assert completed.status_code == 200
assert completed.json()["completed_at"]
assert client.post("/api/materials", json={
"title": "Нельзя создать", "kind": "text", "body": "запрещено",
}).status_code == 403
def test_teacher_cannot_edit_archive_or_assign_another_teachers_material(client, monkeypatch):
from app.api.auth import Principal
from app.api.http import materials as materials_api
from app.domain.roles import Role
identity = {"login": "teacher-one"}
def instructor(_request, *_roles):
return Principal(login=identity["login"], full_name="Преподаватель", role=Role.INSTRUCTOR)
monkeypatch.setattr(materials_api, "require", instructor)
created = client.post("/api/materials", json={
"title": "Личный материал",
"level": "L1",
"kind": "text",
"body": "Учебный текст.",
})
assert created.status_code == 201, created.text
material_id = created.json()["id"]
identity["login"] = "teacher-two"
assert client.patch(f"/api/materials/{material_id}", json={"title": "Подмена"}).status_code == 404
assert client.delete(f"/api/materials/{material_id}").status_code == 404
assert client.put(
f"/api/materials/{material_id}/assign/00000000-0000-4000-8000-000000000112"
).status_code == 404
assert client.delete(
f"/api/materials/{material_id}/assign/00000000-0000-4000-8000-000000000112"
).status_code == 404
def test_uploaded_file_is_limited_sanitized_and_downloaded_as_attachment(client):
_instructor(client)
content = b"local training resource\n"
created = client.post("/api/materials", json={
"title": "Локальная памятка PDF",
"kind": "file",
"file_name": "C:\\Users\\teacher\\guide.txt",
"media_type": "text/plain",
"content_base64": base64.b64encode(content).decode(),
})
assert created.status_code == 201, created.text
data = created.json()
assert data["file_name"] == "guide.txt"
assert data["file_size"] == len(content)
assert len(data["file_sha256"]) == 64
downloaded = client.get(f"/api/materials/{data['id']}/download")
assert downloaded.status_code == 200
assert downloaded.content == content
assert downloaded.headers["x-content-type-options"] == "nosniff"
assert downloaded.headers["content-disposition"].startswith("attachment")
def test_unassigned_trainee_cannot_download_resource(client):
_instructor(client)
created = client.post("/api/materials", json={
"title": "Закрытый ресурс",
"kind": "file",
"file_name": "private.pdf",
"media_type": "application/pdf",
"content_base64": base64.b64encode(b"%PDF-demo").decode(),
})
material_id = created.json()["id"]
_trainee(client)
assert client.get(f"/api/materials/{material_id}/download").status_code == 403
def test_archive_hides_material_from_trainee_but_keeps_record(client):
_instructor(client)
seeded = client.get("/api/materials").json()[0]
archived = client.delete(f"/api/materials/{seeded['id']}")
assert archived.status_code == 200
assert archived.json()["active"] is False
assert client.get("/api/materials").json() == []
archived_list = client.get("/api/materials?include_archived=true").json()
assert len(archived_list) == 1 and archived_list[0]["active"] is False
_trainee(client)
assert client.get("/api/materials").json() == []
def test_trainee_starts_assigned_practice_in_self_mode(client):
_trainee(client)
seeded = client.get("/api/materials").json()[0]
started = client.post(f"/api/materials/{seeded['id']}/start")
assert started.status_code == 200, started.text
payload = started.json()
assert payload["mode"] == "self"
assert payload["exercise"] == "card"
assert payload["path"].startswith("/trainee?session=")
state = hub.get(UUID(payload["session_id"]))
assert state is not None
assert state.trainee_id.hex == "00000000000040008000000000000112"
hub.drop(state.session_id)