276 lines
9.0 KiB
Python
276 lines
9.0 KiB
Python
"""
|
|
Course domain model tests (TRA-236).
|
|
|
|
Covers:
|
|
- Migration smoke (tables exist after migrate)
|
|
- Course → Module → Lesson → Page relation integrity
|
|
- Ordering defaults and cascade deletes
|
|
- Ordering uniqueness constraints (unique_together on (parent, order))
|
|
- required_seconds field on Page
|
|
- version fields on all models
|
|
|
|
Tests that touch the DB are marked @pytest.mark.django_db.
|
|
"""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
from django.db import IntegrityError, transaction
|
|
|
|
from courses.models import Course, Lesson, Module, Page
|
|
|
|
|
|
# ── Fixtures ──────────────────────────────────────────────────────────────────
|
|
|
|
ORG_ID = uuid.uuid4()
|
|
|
|
|
|
def _make_course(**kwargs):
|
|
defaults = dict(
|
|
org_id=ORG_ID,
|
|
title="Test Course",
|
|
slug=f"test-course-{uuid.uuid4().hex[:8]}",
|
|
description="",
|
|
version=1,
|
|
is_published=False,
|
|
)
|
|
defaults.update(kwargs)
|
|
return Course.objects.create(**defaults)
|
|
|
|
|
|
def _make_module(course, order=0, **kwargs):
|
|
defaults = {"title": "Module A", "order": order}
|
|
defaults.update(kwargs)
|
|
return Module.objects.create(course=course, **defaults)
|
|
|
|
|
|
def _make_lesson(module, order=0, **kwargs):
|
|
defaults = {"title": "Lesson A", "order": order}
|
|
defaults.update(kwargs)
|
|
return Lesson.objects.create(module=module, **defaults)
|
|
|
|
|
|
def _make_page(lesson, order=0, **kwargs):
|
|
defaults = {"title": "Page A", "order": order}
|
|
defaults.update(kwargs)
|
|
return Page.objects.create(lesson=lesson, **defaults)
|
|
|
|
|
|
# ── Migration / table existence ───────────────────────────────────────────────
|
|
|
|
@pytest.mark.django_db
|
|
class TestMigrationSmoke:
|
|
"""Tables must exist and be writable after the initial migration."""
|
|
|
|
def test_course_table_exists(self):
|
|
c = _make_course()
|
|
assert Course.objects.filter(pk=c.pk).exists()
|
|
|
|
def test_module_table_exists(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
assert Module.objects.filter(pk=m.pk).exists()
|
|
|
|
def test_lesson_table_exists(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
assert Lesson.objects.filter(pk=lesson.pk).exists()
|
|
|
|
def test_page_table_exists(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
p = _make_page(lesson)
|
|
assert Page.objects.filter(pk=p.pk).exists()
|
|
|
|
def test_uuid_primary_keys(self):
|
|
c = _make_course()
|
|
assert isinstance(c.pk, uuid.UUID)
|
|
|
|
def test_version_field_default(self):
|
|
c = _make_course()
|
|
assert c.version == 1
|
|
m = _make_module(c)
|
|
assert m.version == 1
|
|
|
|
|
|
# ── Relation integrity ────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.django_db
|
|
class TestRelationIntegrity:
|
|
"""FK relationships and cascade deletes."""
|
|
|
|
def test_module_references_course(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
assert m.course_id == c.pk
|
|
|
|
def test_lesson_references_module(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
assert lesson.module_id == m.pk
|
|
|
|
def test_page_references_lesson(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
p = _make_page(lesson)
|
|
assert p.lesson_id == lesson.pk
|
|
|
|
def test_reverse_relations_populated(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
p = _make_page(lesson)
|
|
|
|
assert c.modules.filter(pk=m.pk).exists()
|
|
assert m.lessons.filter(pk=lesson.pk).exists()
|
|
assert lesson.pages.filter(pk=p.pk).exists()
|
|
|
|
def test_cascade_delete_module_removes_lessons(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
m.delete()
|
|
assert not Lesson.objects.filter(pk=lesson.pk).exists()
|
|
|
|
def test_cascade_delete_lesson_removes_pages(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
p = _make_page(lesson)
|
|
lesson.delete()
|
|
assert not Page.objects.filter(pk=p.pk).exists()
|
|
|
|
def test_cascade_delete_course_removes_all(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
p = _make_page(lesson)
|
|
c.delete()
|
|
assert not Module.objects.filter(pk=m.pk).exists()
|
|
assert not Lesson.objects.filter(pk=lesson.pk).exists()
|
|
assert not Page.objects.filter(pk=p.pk).exists()
|
|
|
|
|
|
# ── Ordering ──────────────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.django_db
|
|
class TestOrdering:
|
|
"""Default queryset ordering is by `order` field."""
|
|
|
|
def test_modules_ordered_by_order(self):
|
|
c = _make_course()
|
|
m2 = _make_module(c, order=2, title="Module B")
|
|
m0 = _make_module(c, order=0, title="Module A")
|
|
m1 = _make_module(c, order=1, title="Module C")
|
|
|
|
pks = list(c.modules.values_list("pk", flat=True))
|
|
assert pks == [m0.pk, m1.pk, m2.pk]
|
|
|
|
def test_lessons_ordered_by_order(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
l1 = _make_lesson(m, order=1, title="B")
|
|
l0 = _make_lesson(m, order=0, title="A")
|
|
|
|
pks = list(m.lessons.values_list("pk", flat=True))
|
|
assert pks == [l0.pk, l1.pk]
|
|
|
|
def test_pages_ordered_by_order(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
p2 = _make_page(lesson, order=2, title="C")
|
|
p0 = _make_page(lesson, order=0, title="A")
|
|
p1 = _make_page(lesson, order=1, title="B")
|
|
|
|
pks = list(lesson.pages.values_list("pk", flat=True))
|
|
assert pks == [p0.pk, p1.pk, p2.pk]
|
|
|
|
|
|
# ── Ordering uniqueness constraints ───────────────────────────────────────────
|
|
|
|
@pytest.mark.django_db
|
|
class TestOrderingConstraints:
|
|
"""(parent, order) pairs must be unique within each level."""
|
|
|
|
def test_duplicate_module_order_raises(self):
|
|
c = _make_course()
|
|
_make_module(c, order=0)
|
|
with pytest.raises(IntegrityError):
|
|
with transaction.atomic():
|
|
_make_module(c, order=0)
|
|
|
|
def test_same_order_in_different_courses_allowed(self):
|
|
c1 = _make_course(slug="c1")
|
|
c2 = _make_course(slug="c2")
|
|
_make_module(c1, order=0)
|
|
m2 = _make_module(c2, order=0)
|
|
assert Module.objects.filter(pk=m2.pk).exists()
|
|
|
|
def test_duplicate_lesson_order_raises(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
_make_lesson(m, order=0)
|
|
with pytest.raises(IntegrityError):
|
|
with transaction.atomic():
|
|
_make_lesson(m, order=0)
|
|
|
|
def test_same_lesson_order_in_different_modules_allowed(self):
|
|
c = _make_course()
|
|
m1 = _make_module(c, order=0)
|
|
m2 = _make_module(c, order=1)
|
|
_make_lesson(m1, order=0)
|
|
l2 = _make_lesson(m2, order=0)
|
|
assert Lesson.objects.filter(pk=l2.pk).exists()
|
|
|
|
def test_duplicate_page_order_raises(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
_make_page(lesson, order=0)
|
|
with pytest.raises(IntegrityError):
|
|
with transaction.atomic():
|
|
_make_page(lesson, order=0)
|
|
|
|
def test_same_page_order_in_different_lessons_allowed(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
l1 = _make_lesson(m, order=0)
|
|
l2 = _make_lesson(m, order=1)
|
|
_make_page(l1, order=0)
|
|
p2 = _make_page(l2, order=0)
|
|
assert Page.objects.filter(pk=p2.pk).exists()
|
|
|
|
|
|
# ── required_seconds / navigation gate field ─────────────────────────────────
|
|
|
|
@pytest.mark.django_db
|
|
class TestPageRequiredSeconds:
|
|
"""required_seconds defaults and constraints."""
|
|
|
|
def test_default_required_seconds_is_zero(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
p = _make_page(lesson)
|
|
assert p.required_seconds == 0
|
|
|
|
def test_custom_required_seconds_stored(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
p = _make_page(lesson, required_seconds=120)
|
|
p_db = Page.objects.get(pk=p.pk)
|
|
assert p_db.required_seconds == 120
|
|
|
|
def test_is_required_defaults_true(self):
|
|
c = _make_course()
|
|
m = _make_module(c)
|
|
lesson = _make_lesson(m)
|
|
p = _make_page(lesson)
|
|
assert p.is_required is True
|