Repository navigation
Reorder filters produced by Scope #410
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a1f86b5
8ca4137
95df18d
aa614f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| from morango.models.certificates import Certificate | ||
| from morango.models.certificates import Filter | ||
| from morango.models.certificates import Key | ||
| from morango.models.certificates import Scope | ||
| from morango.models.certificates import ScopeDefinition | ||
|
|
||
|
|
||
|
|
@@ -271,3 +272,47 @@ def test_from_template(self): | |
| def test_from_template__with_json_params(self): | ||
| f = Filter.from_template("test:${param}", '{"param": "value"}') | ||
| self.assertEqual(f._filter_tuple, ("test:value",)) | ||
|
|
||
|
|
||
| class ScopeTestCase(SimpleTestCase): | ||
| params = {"user_id": "U"} | ||
|
|
||
| def _scope(self, read="", write="", read_write=""): | ||
| definition = ScopeDefinition( | ||
| read_filter_template=read, | ||
| write_filter_template=write, | ||
| read_write_filter_template=read_write, | ||
| ) | ||
| return Scope(definition, self.params) | ||
|
|
||
| def test_read_filter__read_write_last(self): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Positive Highlight: |
||
| scope = self._scope( | ||
| read="a:allusers-ro\na:user-ro:${user_id}", | ||
| read_write="a:user-rw:${user_id}\na:rw2", | ||
| ) | ||
| self.assertEqual( | ||
| tuple(scope.read_filter), | ||
| ("a:allusers-ro", "a:user-ro:U", "a:user-rw:U", "a:rw2"), | ||
| ) | ||
|
|
||
| def test_write_filter__read_write_last(self): | ||
| scope = self._scope( | ||
| write="a:user-wo:${user_id}\na:other-wo", | ||
| read_write="a:user-rw:${user_id}\na:rw2", | ||
| ) | ||
| self.assertEqual( | ||
| tuple(scope.write_filter), | ||
| ("a:user-wo:U", "a:other-wo", "a:user-rw:U", "a:rw2"), | ||
| ) | ||
|
|
||
| def test_filters__duplicate_partition(self): | ||
| scope = self._scope( | ||
| read="a:shared\na:user-ro:${user_id}", | ||
| read_write="a:user-rw:${user_id}\na:shared", | ||
| ) | ||
| self.assertEqual(tuple(scope.read_filter), ("a:shared", "a:user-ro:U", "a:user-rw:U")) | ||
|
|
||
| def test_filters__empty_read_write_template(self): | ||
| scope = self._scope(read="a:ro\nb:ro", write="a:wo") | ||
| self.assertEqual(tuple(scope.read_filter), ("a:ro", "b:ro")) | ||
| self.assertEqual(tuple(scope.write_filter), ("a:wo",)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,8 @@ | |
| from morango.errors import MorangoDirtyParent | ||
| from morango.errors import MorangoMissingParent | ||
| from morango.models.certificates import Filter | ||
| from morango.models.certificates import Scope | ||
| from morango.models.certificates import ScopeDefinition | ||
| from morango.models.core import DeletedModels | ||
| from morango.models.core import HardDeletedModels | ||
| from morango.models.core import Store | ||
|
|
@@ -1715,6 +1717,48 @@ def test_sync_filter__partition_order(self): | |
| self._assert_store(log.id, dirty_bit=False) | ||
| self.assertTrue(SummaryLog.objects.filter(id=log.id).exists()) | ||
|
|
||
| def _single_user_scope(self): | ||
| """ | ||
| A scope in the shape of Kolibri's `single-user` scope, with the user in a read-only | ||
| partition and its logs in the read-write partition, which sorts first | ||
| """ | ||
| definition = ScopeDefinition( | ||
| read_filter_template="u:b-ro:${user_id}", | ||
| write_filter_template="", | ||
| read_write_filter_template="u:a-rw:${user_id}", | ||
| ) | ||
| return Scope(definition, {"user_id": "U"}) | ||
|
|
||
| def test_sync_filter__scope_read_filter(self): | ||
| """ | ||
| A `single-user` scope keeps the user in a read-only partition and its logs in the | ||
| read-write partition, and its read filter deserializes the user before the logs | ||
| """ | ||
| user = self._user(partition="u:b-ro:U") | ||
| log = self._summary_log(user.id, partition="u:a-rw:U") | ||
|
|
||
| self._deserialize(sync_filter=self._single_user_scope().read_filter) | ||
|
|
||
| self._assert_store(user.id, dirty_bit=False) | ||
| self._assert_store(log.id, dirty_bit=False) | ||
| self.assertTrue(SummaryLog.objects.filter(id=log.id).exists()) | ||
|
|
||
| def test_sync_filter__read_write_partition_first(self): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Positive Highlight: The counter-example is well designed — |
||
| """ | ||
| A filter that lists the read-write partition first, the order `Scope` used before, | ||
| deserializes the log before its user, so the log fails | ||
| """ | ||
| user = self._user(partition="u:b-ro:U") | ||
| log = self._summary_log(user.id, partition="u:a-rw:U") | ||
|
|
||
| self._deserialize(sync_filter=Filter("u:a-rw:U\nu:b-ro:U")) | ||
|
|
||
| self._assert_store(user.id, dirty_bit=False) | ||
| self._assert_store( | ||
| log.id, dirty_bit=True, exception="django.core.exceptions.ValidationError" | ||
| ) | ||
| self.assertFalse(SummaryLog.objects.exists()) | ||
|
|
||
| def test_summary_logged(self): | ||
| self._user() | ||
| self._facility(serialized="{bad") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ Positive Highlight: This is the core of the fix.
Filter.__add__deduplicates while keeping the first occurrence's position, so the read/write template partitions keep their own order and the read-write partitions land last. I verified the order-insensitive consumers stay correct:Filter.__eq__andis_subset_ofcompare partitions without regard to order (used byCertificate.check_certificate, the viewsets scope checks, andScope.__eq__), andlock_partitionslocks asetof partitions. Certificate IDs and signatures are unchanged because certificates store the templates, not the composed filter.