Skip to content

Commit 44810a7

Browse files
committed
test: add unit tests for model
1 parent 96f916d commit 44810a7

1 file changed

Lines changed: 119 additions & 0 deletions

File tree

openedx_authz/tests/test_models.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
This test suite verifies the functionality of the authorization models including:
44
- ExtendedCasbinRule model with metadata and relationships
55
- Cascade deletion behavior across model hierarchies
6+
- AuthzCourseAuthoringMigrationRun (migration tracking and uniqueness on RUNNING inserts)
67
78
These tests use the stub ContentLibrary model from openedx_authz.tests.stubs.models
89
instead of the real ContentLibrary model, allowing them to run without the full
@@ -18,12 +19,19 @@
1819

1920
from casbin_adapter.models import CasbinRule
2021
from django.contrib.auth import get_user_model
22+
from django.db import IntegrityError
2123
from django.test import TestCase
2224
from opaque_keys.edx.keys import CourseKey
2325
from opaque_keys.edx.locator import LibraryLocatorV2
2426

2527
from openedx_authz.api.data import ContentLibraryData, CourseOverviewData, UserData
2628
from openedx_authz.models import ExtendedCasbinRule, Scope, Subject
29+
from openedx_authz.models.authz_migration import (
30+
AuthzCourseAuthoringMigrationRun,
31+
MigrationType,
32+
ScopeType,
33+
Status,
34+
)
2735
from openedx_authz.models.engine import PolicyCacheControl
2836
from openedx_authz.tests.stubs.models import ContentLibrary, CourseOverview
2937

@@ -311,3 +319,114 @@ def test_singleton_behavior(self):
311319
instance1.save()
312320
all_instances = PolicyCacheControl.objects.all()
313321
self.assertEqual(all_instances.count(), 1)
322+
323+
324+
class TestAuthzCourseAuthoringMigrationRun(TestCase):
325+
"""Tests for ``AuthzCourseAuthoringMigrationRun`` lifecycle and uniqueness rules."""
326+
327+
def setUp(self):
328+
self.scope_key = "course-v1:TestOrg+AuthzMigrationRun+2024"
329+
330+
def test_create_running(self):
331+
"""``create_running`` stores RUNNING and optional metadata."""
332+
meta = {"batch": 1}
333+
334+
run = AuthzCourseAuthoringMigrationRun.create_running(
335+
MigrationType.FORWARD, ScopeType.COURSE, self.scope_key, metadata=meta
336+
)
337+
338+
run.refresh_from_db()
339+
self.assertEqual(run.migration_type, MigrationType.FORWARD)
340+
self.assertEqual(run.scope_type, ScopeType.COURSE)
341+
self.assertEqual(run.scope_key, self.scope_key)
342+
self.assertEqual(run.status, Status.RUNNING)
343+
self.assertEqual(run.metadata, meta)
344+
345+
def test_create_skipped_merges_skip_reason(self):
346+
"""``create_skipped`` records SKIPPED and documents why."""
347+
run = AuthzCourseAuthoringMigrationRun.create_skipped(
348+
MigrationType.ROLLBACK, ScopeType.ORG, "test_org", metadata={"note": "extra"}
349+
)
350+
351+
self.assertEqual(run.status, Status.SKIPPED)
352+
self.assertEqual(run.metadata["note"], "extra")
353+
self.assertIn("skip_reason", run.metadata)
354+
355+
def test_str_representation(self):
356+
"""``__str__`` includes id, migration type, scope, and status."""
357+
run = AuthzCourseAuthoringMigrationRun.create_running(MigrationType.FORWARD, ScopeType.COURSE, self.scope_key)
358+
359+
text = str(run)
360+
self.assertIn(str(run.id), text)
361+
self.assertIn(MigrationType.FORWARD.value, text)
362+
self.assertIn(self.scope_key, text)
363+
self.assertIn(Status.RUNNING.value, text)
364+
365+
def test_second_running_insert_same_scope_raises_integrity_error(self):
366+
"""At most one RUNNING row per (scope_type, scope_key) on insert."""
367+
AuthzCourseAuthoringMigrationRun.create_running(MigrationType.FORWARD, ScopeType.COURSE, self.scope_key)
368+
369+
with self.assertRaises(IntegrityError):
370+
AuthzCourseAuthoringMigrationRun.create_running(MigrationType.FORWARD, ScopeType.COURSE, self.scope_key)
371+
372+
def test_running_allowed_after_previous_run_finished(self):
373+
"""A new RUNNING row is allowed once the prior run for that scope is no longer RUNNING."""
374+
first = AuthzCourseAuthoringMigrationRun.create_running(MigrationType.FORWARD, ScopeType.COURSE, self.scope_key)
375+
first.mark_completed()
376+
377+
second = AuthzCourseAuthoringMigrationRun.create_running(
378+
MigrationType.FORWARD, ScopeType.COURSE, self.scope_key
379+
)
380+
381+
self.assertEqual(second.status, Status.RUNNING)
382+
self.assertNotEqual(first.pk, second.pk)
383+
384+
def test_distinct_scope_type_allows_parallel_running_same_key_string(self):
385+
"""RUNNING uniqueness is per (scope_type, scope_key), not key alone."""
386+
key = "same-key-string"
387+
388+
AuthzCourseAuthoringMigrationRun.create_running(MigrationType.FORWARD, ScopeType.COURSE, key)
389+
other = AuthzCourseAuthoringMigrationRun.create_running(MigrationType.FORWARD, ScopeType.ORG, key)
390+
391+
self.assertEqual(other.status, Status.RUNNING)
392+
393+
def test_mark_completed_and_partial_success_merge_metadata(self):
394+
"""Finalizers set status, ``completed_at``, and merge metadata."""
395+
run = AuthzCourseAuthoringMigrationRun.create_running(
396+
MigrationType.FORWARD, ScopeType.COURSE, self.scope_key, metadata={"a": 1}
397+
)
398+
run.mark_completed(metadata_updates={"success_count": 3})
399+
400+
run.refresh_from_db()
401+
self.assertEqual(run.status, Status.COMPLETED)
402+
self.assertIsNotNone(run.completed_at)
403+
self.assertEqual(run.metadata["a"], 1)
404+
self.assertEqual(run.metadata["success_count"], 3)
405+
406+
run2 = AuthzCourseAuthoringMigrationRun.create_running(MigrationType.ROLLBACK, ScopeType.ORG, "org_partial")
407+
run2.mark_partial_success(metadata_updates={"error_count": 2})
408+
409+
run2.refresh_from_db()
410+
self.assertEqual(run2.status, Status.PARTIAL_SUCCESS)
411+
self.assertEqual(run2.metadata["error_count"], 2)
412+
413+
def test_mark_failed_with_and_without_exception(self):
414+
"""``mark_failed`` records FAILED, optional exception string is stored when provided."""
415+
run = AuthzCourseAuthoringMigrationRun.create_running(
416+
MigrationType.FORWARD, ScopeType.COURSE, f"{self.scope_key}_fail"
417+
)
418+
run.mark_failed(exception=ValueError("boom"))
419+
420+
run.refresh_from_db()
421+
self.assertEqual(run.status, Status.FAILED)
422+
self.assertEqual(run.metadata["error"], "boom")
423+
424+
run2 = AuthzCourseAuthoringMigrationRun.create_running(
425+
MigrationType.FORWARD, ScopeType.COURSE, f"{self.scope_key}_fail2"
426+
)
427+
prior_meta = dict(run2.metadata)
428+
run2.mark_failed()
429+
430+
run2.refresh_from_db()
431+
self.assertEqual(run2.status, Status.FAILED)
432+
self.assertEqual(run2.metadata, prior_meta)

0 commit comments

Comments
 (0)