Files
training-software/tests/test_course_domain.py
Paperclip CTO 01661bf5a8
Some checks failed
CI / lint (push) Successful in 6s
CI / test (push) Failing after 7s
CI / build-container (push) Has been skipped
fix: restore CI by fixing lint, test collection, and notification syntax
2026-05-18 14:53:29 +02:00

270 lines
8.8 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):
return Module.objects.create(course=course, title="Module A", order=order, **kwargs)
def _make_lesson(module, order=0, **kwargs):
return Lesson.objects.create(module=module, title="Lesson A", order=order, **kwargs)
def _make_page(lesson, order=0, **kwargs):
return Page.objects.create(lesson=lesson, title="Page A", order=order, **kwargs)
# ── 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