lct-hack/backend/tests/test_materials.py
2026-09-26 17:13:45 +00:00

176 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Учебные материалы: создание, назначение, изучение и безопасная загрузка."""
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)
created = client.post("/api/materials", json={
"title": "Архивируемая памятка",
"kind": "text",
"body": "Уникальный тестовый материал для проверки архивации.",
})
assert created.status_code == 201, created.text
material_id = created.json()["id"]
archived = client.delete(f"/api/materials/{material_id}")
assert archived.status_code == 200
assert archived.json()["active"] is False
assert all(item["id"] != material_id for item in client.get("/api/materials").json())
archived_list = client.get("/api/materials?include_archived=true").json()
archived_item = next(item for item in archived_list if item["id"] == material_id)
assert archived_item["active"] is False
_trainee(client)
assert all(item["id"] != material_id for item in 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)