MDL-63619 tool_dataprivacy: Fix inheritance from parent contexts
authorAndrew Nicols <andrew@nicols.co.uk>
Mon, 15 Oct 2018 02:45:31 +0000 (10:45 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 24 Oct 2018 00:48:18 +0000 (08:48 +0800)
Inheritance should behave such that all contexts inherit from their
parent context.

Prior to this fix, if the value was not set on a context, then it was
getting a default of 'Inherit', but instead of inheritting from the
parent context, it was inheritting from its parent context _level_ which
is just wrong.

admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/data_registry.php
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/behat/manage_defaults.feature
admin/tool/dataprivacy/tests/data_registry_test.php [new file with mode: 0644]

index aef98de..e73a34c 100644 (file)
@@ -914,7 +914,7 @@ class api {
      * @param int $forcedvalue Use this categoryid value as if this was this context instance category.
      * @return category|false
      */
-    public static function get_effective_context_category(\context $context, $forcedvalue=false) {
+    public static function get_effective_context_category(\context $context, $forcedvalue = false) {
         if (!data_registry::defaults_set()) {
             return false;
         }
@@ -941,15 +941,14 @@ class api {
      * Returns the effective category given a context level.
      *
      * @param int $contextlevel
-     * @param int $forcedvalue Use this categoryid value as if this was this context level category.
      * @return category|false
      */
-    public static function get_effective_contextlevel_category($contextlevel, $forcedvalue=false) {
+    public static function get_effective_contextlevel_category($contextlevel) {
         if (!data_registry::defaults_set()) {
             return false;
         }
 
-        return data_registry::get_effective_contextlevel_value($contextlevel, 'category', $forcedvalue);
+        return data_registry::get_effective_contextlevel_value($contextlevel, 'category');
     }
 
     /**
index d10ab83..2865aa0 100644 (file)
@@ -39,18 +39,6 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class data_registry {
-
-    /**
-     * @var array Inheritance between context levels.
-     */
-    private static $contextlevelinheritance = [
-        CONTEXT_USER => [CONTEXT_SYSTEM],
-        CONTEXT_COURSECAT => [CONTEXT_SYSTEM],
-        CONTEXT_COURSE => [CONTEXT_COURSECAT, CONTEXT_SYSTEM],
-        CONTEXT_MODULE => [CONTEXT_COURSE, CONTEXT_COURSECAT, CONTEXT_SYSTEM],
-        CONTEXT_BLOCK => [CONTEXT_COURSE, CONTEXT_COURSECAT, CONTEXT_SYSTEM],
-    ];
-
     /**
      * Returns purpose and category var names from a context class name
      *
@@ -83,7 +71,6 @@ class data_registry {
      * @return int[]|false[]
      */
     public static function get_defaults($contextlevel, $pluginname = '') {
-
         $classname = \context_helper::get_class_for_level($contextlevel);
         list($purposevar, $categoryvar) = self::var_names_from_context($classname, $pluginname);
 
@@ -104,10 +91,10 @@ class data_registry {
         }
 
         if (empty($purposeid)) {
-            $purposeid = false;
+            $purposeid = context_instance::NOTSET;
         }
         if (empty($categoryid)) {
-            $categoryid = false;
+            $categoryid = context_instance::NOTSET;
         }
 
         return [$purposeid, $categoryid];
@@ -190,19 +177,24 @@ class data_registry {
      * @return persistent|false It return a 'purpose' instance or a 'category' instance, depending on $element
      */
     public static function get_effective_context_value(\context $context, $element, $forcedvalue = false) {
-
         if ($element !== 'purpose' && $element !== 'category') {
             throw new coding_exception('Only \'purpose\' and \'category\' are supported.');
         }
         $fieldname = $element . 'id';
 
-        if (empty($forcedvalue)) {
-            $instance = context_instance::get_record_by_contextid($context->id, false);
+        // Check whether this context is a user context, or a child of a user context.
+        // User contexts share the same context and cannot be set individually.
+        $parents = $context->get_parent_contexts(true);
+        foreach ($parents as $parent) {
+            if ($parent->contextlevel == CONTEXT_USER) {
+                // Use the context level value for the user.
+                return self::get_effective_contextlevel_value(CONTEXT_USER, $element);
+            }
+        }
 
-            if (!$instance) {
-                // If the instance does not have a value defaults to not set, so we grab the context level default as its value.
-                $instancevalue = context_instance::NOTSET;
-            } else {
+        $instancevalue = context_instance::NOTSET;
+        if (empty($forcedvalue)) {
+            if ($instance = context_instance::get_record_by_contextid($context->id, false)) {
                 $instancevalue = $instance->get($fieldname);
             }
         } else {
@@ -211,48 +203,34 @@ class data_registry {
 
         // Not set.
         if ($instancevalue == context_instance::NOTSET) {
-
-            // The effective value varies depending on the context level.
-            if ($context->contextlevel == CONTEXT_USER) {
-                // Use the context level value as we don't allow people to set specific instances values.
-                return self::get_effective_contextlevel_value(CONTEXT_USER, $element);
-            }
-
-            $parents = $context->get_parent_contexts(true);
-            foreach ($parents as $parent) {
-                if ($parent->contextlevel == CONTEXT_USER) {
-                    // Use the context level value as we don't allow people to set specific instances values.
-                    return self::get_effective_contextlevel_value(CONTEXT_USER, $element);
-                }
-            }
-
             // Check if we need to pass the plugin name of an activity.
             $forplugin = '';
             if ($context->contextlevel == CONTEXT_MODULE) {
                 list($course, $cm) = get_course_and_cm_from_cmid($context->instanceid);
                 $forplugin = $cm->modname;
             }
+
             // Use the default context level value.
             list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category(
                 $context->contextlevel, false, false, $forplugin
             );
-
-            return self::get_element_instance($element, $$fieldname);
+            $instancevalue = $$fieldname;
         }
 
-        // Specific value for this context instance.
-        if ($instancevalue != context_instance::INHERIT) {
+        if (context_instance::NOTSET !== $instancevalue && context_instance::INHERIT !== $instancevalue) {
+            // There is an actual value. Return it.
             return self::get_element_instance($element, $instancevalue);
         }
 
-        // This context is using inherited so let's return the parent effective value.
-        $parentcontext = $context->get_parent_context();
-        if (!$parentcontext) {
-            return false;
-        }
+        // There is no value set (or it is set to inherit).
+        // Fetch from the parent context.
+        $parent = $context->get_parent_context();
 
-        // The forced value should not be transmitted to parent contexts.
-        return self::get_effective_context_value($parentcontext, $element);
+        if (CONTEXT_SYSTEM == $parent->contextlevel) {
+            return self::get_effective_contextlevel_value(CONTEXT_SYSTEM, $element);
+        } else {
+            return self::get_effective_context_value($context->get_parent_context(), $element);
+        }
     }
 
     /**
@@ -264,11 +242,9 @@ class data_registry {
      *
      * @param int $contextlevel
      * @param string $element 'category' or 'purpose'
-     * @param int $forcedvalue Use this value as if this was this context level purpose.
      * @return \tool_dataprivacy\purpose|false
      */
-    public static function get_effective_contextlevel_value($contextlevel, $element, $forcedvalue = false) {
-
+    public static function get_effective_contextlevel_value($contextlevel, $element) {
         if ($element !== 'purpose' && $element !== 'category') {
             throw new coding_exception('Only \'purpose\' and \'category\' are supported.');
         }
@@ -279,39 +255,15 @@ class data_registry {
                 'have a purpose or a category.');
         }
 
-        if ($forcedvalue === false) {
-            $instance = contextlevel::get_record_by_contextlevel($contextlevel, false);
-            if (!$instance) {
-                // If the context level does not have a value defaults to not set, so we grab the context level default as
-                // its value.
-                $instancevalue = context_instance::NOTSET;
-            } else {
-                $instancevalue = $instance->get($fieldname);
-            }
-        } else {
-            $instancevalue = $forcedvalue;
-        }
+        list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category($contextlevel);
 
-        // Not set -> Use the default context level value.
-        if ($instancevalue == context_instance::NOTSET) {
-            list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category($contextlevel);
+        // Note: The $$fieldname points to either $purposeid, or $categoryid.
+        if (context_instance::NOTSET !== $$fieldname && context_instance::INHERIT !== $$fieldname) {
+            // There is a specific value set.
             return self::get_element_instance($element, $$fieldname);
         }
 
-        // Specific value for this context instance.
-        if ($instancevalue != context_instance::INHERIT) {
-            return self::get_element_instance($element, $instancevalue);
-        }
-
-        if ($contextlevel == CONTEXT_SYSTEM) {
-            throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.');
-        }
-
-        // If we reach this point is that we are inheriting so get the parent context level and repeat.
-        $parentcontextlevel = reset(self::$contextlevelinheritance[$contextlevel]);
-
-        // Forced value are intentionally not passed as the force value should only affect the immediate context level.
-        return self::get_effective_contextlevel_value($parentcontextlevel, $element);
+        throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.');
     }
 
     /**
@@ -320,13 +272,13 @@ class data_registry {
      * @param int $contextlevel
      * @param int|bool $forcedpurposevalue Use this value as if this was this context level purpose.
      * @param int|bool $forcedcategoryvalue Use this value as if this was this context level category.
-     * @param string $activity The plugin name of the activity.
+     * @param string $component The name of the component to check.
      * @return int[]
      */
     public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false,
-                                                                                   $forcedcategoryvalue = false, $activity = '') {
-
-        list($purposeid, $categoryid) = self::get_defaults($contextlevel, $activity);
+                                                                                   $forcedcategoryvalue = false, $component = '') {
+        // Get the defaults for this context level.
+        list($purposeid, $categoryid) = self::get_defaults($contextlevel, $component);
 
         // Honour forced values.
         if ($forcedpurposevalue) {
@@ -336,37 +288,19 @@ class data_registry {
             $categoryid = $forcedcategoryvalue;
         }
 
-        // Not set == INHERIT for defaults.
-        if ($purposeid == context_instance::INHERIT || $purposeid == context_instance::NOTSET) {
-            $purposeid = false;
-        }
-        if ($categoryid == context_instance::INHERIT || $categoryid == context_instance::NOTSET) {
-            $categoryid = false;
-        }
+        if ($contextlevel == CONTEXT_USER) {
+            // Only user context levels inherit from a parent context level.
+            list($parentpurposeid, $parentcategoryid) = self::get_defaults(CONTEXT_SYSTEM);
 
-        if ($contextlevel != CONTEXT_SYSTEM && ($purposeid === false || $categoryid === false)) {
-            foreach (self::$contextlevelinheritance[$contextlevel] as $parent) {
-
-                list($parentpurposeid, $parentcategoryid) = self::get_defaults($parent);
-                // Not set == INHERIT for defaults.
-                if ($parentpurposeid == context_instance::INHERIT || $parentpurposeid == context_instance::NOTSET) {
-                    $parentpurposeid = false;
-                }
-                if ($parentcategoryid == context_instance::INHERIT || $parentcategoryid == context_instance::NOTSET) {
-                    $parentcategoryid = false;
-                }
-
-                if ($purposeid === false && $parentpurposeid) {
-                    $purposeid = $parentpurposeid;
-                }
-
-                if ($categoryid === false && $parentcategoryid) {
-                    $categoryid = $parentcategoryid;
-                }
+            if (context_instance::INHERIT == $purposeid || context_instance::NOTSET == $purposeid) {
+                $purposeid = $parentpurposeid;
+            }
+
+            if (context_instance::INHERIT == $categoryid || context_instance::NOTSET == $categoryid) {
+                $categoryid = $parentcategoryid;
             }
         }
 
-        // They may still be false, but we return anyway.
         return [$purposeid, $categoryid];
     }
 
@@ -379,7 +313,6 @@ class data_registry {
      * @return \core\persistent
      */
     private static function get_element_instance($element, $id) {
-
         if ($element !== 'purpose' && $element !== 'category') {
             throw new coding_exception('No other elements than purpose and category are allowed');
         }
index a290b27..f0617d9 100644 (file)
@@ -1150,11 +1150,6 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->assertEquals($purposes[0]->get('id'), $purposeid);
         $this->assertEquals(false, $categoryid);
 
-        // Course inherits from system if not defined.
-        list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE);
-        $this->assertEquals($purposes[0]->get('id'), $purposeid);
-        $this->assertEquals(false, $categoryid);
-
         // Course defined values should have preference.
         list($purposevar, $categoryvar) = data_registry::var_names_from_context(
             \context_helper::get_class_for_level(CONTEXT_COURSE)
@@ -1168,159 +1163,293 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
 
         // Context level defaults are also allowed to be set to 'inherit'.
         set_config($purposevar, context_instance::INHERIT, 'tool_dataprivacy');
+    }
 
-        list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE);
-        $this->assertEquals($purposes[0]->get('id'), $purposeid);
-        $this->assertEquals($categories[0]->get('id'), $categoryid);
+    /**
+     * Ensure that when nothing is configured, all values return false.
+     */
+    public function test_get_effective_contextlevel_unset() {
+        // Before setup, get_effective_contextlevel_purpose will return false.
+        $this->assertFalse(api::get_effective_contextlevel_category(CONTEXT_SYSTEM));
+        $this->assertFalse(api::get_effective_contextlevel_purpose(CONTEXT_SYSTEM));
 
-        list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_MODULE);
-        $this->assertEquals($purposes[0]->get('id'), $purposeid);
-        $this->assertEquals($categories[0]->get('id'), $categoryid);
+        $this->assertFalse(api::get_effective_contextlevel_category(CONTEXT_USER));
+        $this->assertFalse(api::get_effective_contextlevel_purpose(CONTEXT_USER));
     }
 
-    public function test_get_effective_contextlevel_category() {
+    /**
+     * Ensure that when nothing is configured, all values return false.
+     */
+    public function test_get_effective_context_unset() {
         // Before setup, get_effective_contextlevel_purpose will return false.
-        $this->assertFalse(api::get_effective_contextlevel_category(CONTEXT_SYSTEM));
+        $this->assertFalse(api::get_effective_context_category(\context_system::instance()));
+        $this->assertFalse(api::get_effective_context_purpose(\context_system::instance()));
+    }
+
+    /**
+     * Ensure that fetching the effective value for context levels is only available to system, and user context levels.
+     *
+     * @dataProvider invalid_effective_contextlevel_provider
+     * @param   int $contextlevel
+     */
+    public function test_set_contextlevel_invalid_contextlevels($contextlevel) {
+
+        $this->expectException(coding_exception::class);
+        api::set_contextlevel((object) [
+                'contextlevel' => $contextlevel,
+            ]);
+
     }
 
     /**
      * Test effective contextlevel return.
      */
     public function test_effective_contextlevel() {
-        $this->setAdminUser();
-
         $this->resetAfterTest();
 
-        // Before setup, get_effective_contextlevel_purpose will return false.
-        $this->assertFalse(api::get_effective_contextlevel_purpose(CONTEXT_SYSTEM));
+        // Set the initial purpose and category.
+        $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
+        $category1 = api::create_category((object)['name' => 'a']);
+        api::set_contextlevel((object)[
+            'contextlevel' => CONTEXT_SYSTEM,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
 
-        list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
+        $this->assertEquals($purpose1, api::get_effective_contextlevel_purpose(CONTEXT_SYSTEM));
+        $this->assertEquals($category1, api::get_effective_contextlevel_category(CONTEXT_SYSTEM));
 
-        // Set the system context level to purpose 1.
-        $record = (object)[
-            'contextlevel' => CONTEXT_SYSTEM,
-            'purposeid' => $purposes[1]->get('id'),
-            'categoryid' => $categories[1]->get('id'),
-        ];
-        api::set_contextlevel($record);
+        // The user context inherits from the system context when not set.
+        $this->assertEquals($purpose1, api::get_effective_contextlevel_purpose(CONTEXT_USER));
+        $this->assertEquals($category1, api::get_effective_contextlevel_category(CONTEXT_USER));
 
-        $purpose = api::get_effective_contextlevel_purpose(CONTEXT_SYSTEM);
-        $this->assertEquals($purposes[1]->get('id'), $purpose->get('id'));
+        // Forcing the behaviour to inherit will have the same result.
+        api::set_contextlevel((object) [
+                'contextlevel' => CONTEXT_USER,
+                'purposeid' => context_instance::INHERIT,
+                'categoryid' => context_instance::INHERIT,
+            ]);
+        $this->assertEquals($purpose1, api::get_effective_contextlevel_purpose(CONTEXT_USER));
+        $this->assertEquals($category1, api::get_effective_contextlevel_category(CONTEXT_USER));
 
-        // Value 'not set' will get the default value for the context level. For context level defaults
-        // both 'not set' and 'inherit' result in inherit, so the parent context (system) default
-        // will be retrieved.
-        $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER);
-        $this->assertEquals($purposes[1]->get('id'), $purpose->get('id'));
+        // Setting specific values will override the inheritance behaviour.
+        $purpose2 = api::create_purpose((object)['name' => 'p2', 'retentionperiod' => 'PT2H', 'lawfulbases' => 'gdpr_art_6_1_a']);
+        $category2 = api::create_category((object)['name' => 'b']);
+        // Set the system context level to purpose 1.
+        api::set_contextlevel((object) [
+                'contextlevel' => CONTEXT_USER,
+                'purposeid' => $purpose2->get('id'),
+                'categoryid' => $category2->get('id'),
+            ]);
 
-        // The behaviour forcing an inherit from context system should result in the same effective
-        // purpose.
-        $record->purposeid = context_instance::INHERIT;
-        $record->contextlevel = CONTEXT_USER;
-        api::set_contextlevel($record);
-        $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER);
-        $this->assertEquals($purposes[1]->get('id'), $purpose->get('id'));
+        $this->assertEquals($purpose2, api::get_effective_contextlevel_purpose(CONTEXT_USER));
+        $this->assertEquals($category2, api::get_effective_contextlevel_category(CONTEXT_USER));
+    }
 
-        $record->purposeid = $purposes[2]->get('id');
-        $record->contextlevel = CONTEXT_USER;
-        api::set_contextlevel($record);
+    /**
+     * Ensure that fetching the effective value for context levels is only available to system, and user context levels.
+     *
+     * @dataProvider invalid_effective_contextlevel_provider
+     * @param   int $contextlevel
+     */
+    public function test_effective_contextlevel_invalid_contextlevels($contextlevel) {
+        $this->resetAfterTest();
 
-        $purpose = api::get_effective_contextlevel_purpose(CONTEXT_USER);
-        $this->assertEquals($purposes[2]->get('id'), $purpose->get('id'));
+        $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
+        $category1 = api::create_category((object)['name' => 'a']);
+        api::set_contextlevel((object)[
+            'contextlevel' => CONTEXT_SYSTEM,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
 
-        // Only system and user allowed.
         $this->expectException(coding_exception::class);
-        $record->contextlevel = CONTEXT_COURSE;
-        $record->purposeid = $purposes[1]->get('id');
-        api::set_contextlevel($record);
+        api::get_effective_contextlevel_purpose($contextlevel);
     }
 
     /**
-     * Test effective context purposes and categories.
-     *
-     * @return null
+     * Data provider for invalid contextlevel fetchers.
      */
-    public function test_effective_context() {
+    public function invalid_effective_contextlevel_provider() {
+        return [
+            [CONTEXT_COURSECAT],
+            [CONTEXT_COURSE],
+            [CONTEXT_MODULE],
+            [CONTEXT_BLOCK],
+        ];
+    }
+
+    /**
+     * Ensure that context inheritance works up the context tree.
+     */
+    public function test_effective_context_inheritance() {
         $this->resetAfterTest();
 
-        $this->setAdminUser();
+        $systemdata = $this->create_and_set_purpose_for_contextlevel('PT1S', CONTEXT_SYSTEM);
+
+        /*
+         * System
+         * - Cat
+         *   - Subcat
+         *     - Course
+         *       - Forum
+         * - User
+         *   - User block
+         */
+        $cat = $this->getDataGenerator()->create_category();
+        $subcat = $this->getDataGenerator()->create_category(['parent' => $cat->id]);
+        $course = $this->getDataGenerator()->create_course(['category' => $subcat->id]);
+        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+        list(, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
 
-        list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories();
+        $user = $this->getDataGenerator()->create_user();
 
-        // Define system defaults (all context levels below will inherit).
-        list($purposevar, $categoryvar) = data_registry::var_names_from_context(
-            \context_helper::get_class_for_level(CONTEXT_SYSTEM)
-        );
-        set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy');
-        set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy');
+        $contextsystem = \context_system::instance();
+        $contextcat = \context_coursecat::instance($cat->id);
+        $contextsubcat = \context_coursecat::instance($subcat->id);
+        $contextcourse = \context_course::instance($course->id);
+        $contextforum = \context_module::instance($forumcm->id);
+        $contextuser = \context_user::instance($user->id);
+
+        // Initially everything is set to Inherit.
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat));
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat));
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse));
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum));
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextuser));
+
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat));
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat));
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse));
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum));
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextuser));
+
+        // When actively set, user will use the specified value.
+        $userdata = $this->create_and_set_purpose_for_contextlevel('PT1S', CONTEXT_USER);
+
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat));
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat));
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse));
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum));
+        $this->assertEquals($userdata->purpose, api::get_effective_context_purpose($contextuser));
+
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat));
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat));
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse));
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum));
+        $this->assertEquals($userdata->category, api::get_effective_context_category($contextuser));
+
+        // Set a context for the top category.
+        $catpurpose = new purpose(0, (object) [
+                'name' => 'Purpose',
+                'retentionperiod' => 'P1D',
+                'lawfulbases' => 'gdpr_art_6_1_a',
+            ]);
+        $catpurpose->save();
+        $catcategory = new category(0, (object) ['name' => 'Category']);
+        $catcategory->save();
+        api::set_context_instance((object) [
+                'contextid' => $contextcat->id,
+                'purposeid' => $catpurpose->get('id'),
+                'categoryid' => $catcategory->get('id'),
+            ]);
 
-        // Define course defaults.
-        list($purposevar, $categoryvar) = data_registry::var_names_from_context(
-            \context_helper::get_class_for_level(CONTEXT_COURSE)
-        );
-        set_config($purposevar, $purposes[1]->get('id'), 'tool_dataprivacy');
-        set_config($categoryvar, $categories[1]->get('id'), 'tool_dataprivacy');
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
+        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat));
+        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat));
+        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcourse));
+        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextforum));
+
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
+        $this->assertEquals($catcategory, api::get_effective_context_category($contextcat));
+        $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat));
+        $this->assertEquals($catcategory, api::get_effective_context_category($contextcourse));
+        $this->assertEquals($catcategory, api::get_effective_context_category($contextforum));
+
+        // Set a context for the sub category.
+        $subcatpurpose = new purpose(0, (object) [
+                'name' => 'Purpose',
+                'retentionperiod' => 'P1D',
+                'lawfulbases' => 'gdpr_art_6_1_a',
+            ]);
+        $subcatpurpose->save();
+        $subcatcategory = new category(0, (object) ['name' => 'Category']);
+        $subcatcategory->save();
+        api::set_context_instance((object) [
+                'contextid' => $contextsubcat->id,
+                'purposeid' => $subcatpurpose->get('id'),
+                'categoryid' => $subcatcategory->get('id'),
+            ]);
 
-        $course0context = \context_course::instance($courses[0]->id);
-        $course1context = \context_course::instance($courses[1]->id);
-        $mod0context = \context_module::instance($modules[0]->cmid);
-        $mod1context = \context_module::instance($modules[1]->cmid);
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
+        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat));
+        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat));
+        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextcourse));
+        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextforum));
+
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
+        $this->assertEquals($catcategory, api::get_effective_context_category($contextcat));
+        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat));
+        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextcourse));
+        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextforum));
+
+        // Set a context for the course.
+        $coursepurpose = new purpose(0, (object) [
+                'name' => 'Purpose',
+                'retentionperiod' => 'P1D',
+                'lawfulbases' => 'gdpr_art_6_1_a',
+            ]);
+        $coursepurpose->save();
+        $coursecategory = new category(0, (object) ['name' => 'Category']);
+        $coursecategory->save();
+        api::set_context_instance((object) [
+                'contextid' => $contextcourse->id,
+                'purposeid' => $coursepurpose->get('id'),
+                'categoryid' => $coursecategory->get('id'),
+            ]);
 
-        // Set course instance values.
-        $record = (object)[
-            'contextid' => $course0context->id,
-            'purposeid' => $purposes[1]->get('id'),
-            'categoryid' => $categories[2]->get('id'),
-        ];
-        api::set_context_instance($record);
-        $category = api::get_effective_context_category($course0context);
-        $this->assertEquals($record->categoryid, $category->get('id'));
-
-        // Module instances get the context level default if nothing specified.
-        $category = api::get_effective_context_category($mod0context);
-        $this->assertEquals($categories[1]->get('id'), $category->get('id'));
-
-        // Module instances get the parent context category if they inherit.
-        $record->contextid = $mod0context->id;
-        $record->categoryid = context_instance::INHERIT;
-        api::set_context_instance($record);
-        $category = api::get_effective_context_category($mod0context);
-        $this->assertEquals($categories[2]->get('id'), $category->get('id'));
-
-        // The $forcedvalue param allows us to override the actual value (method php-docs for more info).
-        $category = api::get_effective_context_category($mod0context, $categories[1]->get('id'));
-        $this->assertEquals($categories[1]->get('id'), $category->get('id'));
-        $category = api::get_effective_context_category($mod0context, $categories[0]->get('id'));
-        $this->assertEquals($categories[0]->get('id'), $category->get('id'));
-
-        // Module instances get the parent context category if they inherit; in
-        // this case the parent context category is not set so it should use the
-        // context level default (see 'Define course defaults' above).
-        $record->contextid = $mod1context->id;
-        $record->categoryid = context_instance::INHERIT;
-        api::set_context_instance($record);
-        $category = api::get_effective_context_category($mod1context);
-        $this->assertEquals($categories[1]->get('id'), $category->get('id'));
-
-        // User instances use the value set at user context level instead of the user default.
-
-        // User defaults to cat 0 and user context level to 1.
-        list($purposevar, $categoryvar) = data_registry::var_names_from_context(
-            \context_helper::get_class_for_level(CONTEXT_USER)
-        );
-        set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy');
-        set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy');
-        $usercontextlevel = (object)[
-            'contextlevel' => CONTEXT_USER,
-            'purposeid' => $purposes[1]->get('id'),
-            'categoryid' => $categories[1]->get('id'),
-        ];
-        api::set_contextlevel($usercontextlevel);
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
+        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat));
+        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat));
+        $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextcourse));
+        $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextforum));
+
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
+        $this->assertEquals($catcategory, api::get_effective_context_category($contextcat));
+        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat));
+        $this->assertEquals($coursecategory, api::get_effective_context_category($contextcourse));
+        $this->assertEquals($coursecategory, api::get_effective_context_category($contextforum));
+
+        // Set a context for the forum.
+        $forumpurpose = new purpose(0, (object) [
+                'name' => 'Purpose',
+                'retentionperiod' => 'P1D',
+                'lawfulbases' => 'gdpr_art_6_1_a',
+            ]);
+        $forumpurpose->save();
+        $forumcategory = new category(0, (object) ['name' => 'Category']);
+        $forumcategory->save();
+        api::set_context_instance((object) [
+                'contextid' => $contextforum->id,
+                'purposeid' => $forumpurpose->get('id'),
+                'categoryid' => $forumcategory->get('id'),
+            ]);
 
-        $newuser = $this->getDataGenerator()->create_user();
-        $usercontext = \context_user::instance($newuser->id);
-        $category = api::get_effective_context_category($usercontext);
-        $this->assertEquals($categories[1]->get('id'), $category->get('id'));
+        $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem));
+        $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat));
+        $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat));
+        $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextcourse));
+        $this->assertEquals($forumpurpose, api::get_effective_context_purpose($contextforum));
+
+        $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem));
+        $this->assertEquals($catcategory, api::get_effective_context_category($contextcat));
+        $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat));
+        $this->assertEquals($coursecategory, api::get_effective_context_category($contextcourse));
+        $this->assertEquals($forumcategory, api::get_effective_context_category($contextforum));
     }
 
     /**
@@ -1388,7 +1517,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->resetAfterTest();
 
         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
-        $purposes->course->set('protected', 1)->save();
+        $purposes->course->purpose->set('protected', 1)->save();
 
         $user = $this->getDataGenerator()->create_user();
         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]);
@@ -1404,7 +1533,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
 
         $request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
 
-        $purposes->course->set('protected', 1)->save();
+        $purposes->course->purpose->set('protected', 1)->save();
         api::add_request_contexts_with_status($collection, $request->get('id'), contextlist_context::STATUS_APPROVED);
 
         $requests = contextlist_context::get_records();
@@ -1420,7 +1549,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->resetAfterTest();
 
         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
-        $purposes->course->set('protected', 1)->save();
+        $purposes->course->purpose->set('protected', 1)->save();
 
         $user = $this->getDataGenerator()->create_user();
         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
@@ -1436,7 +1565,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
 
         $request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
 
-        $purposes->course->set('protected', 1)->save();
+        $purposes->course->purpose->set('protected', 1)->save();
         api::add_request_contexts_with_status($collection, $request->get('id'), contextlist_context::STATUS_APPROVED);
 
         $requests = contextlist_context::get_records();
@@ -1452,7 +1581,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->resetAfterTest();
 
         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
-        $purposes->course->set('protected', 1)->save();
+        $purposes->course->purpose->set('protected', 1)->save();
 
         $user = $this->getDataGenerator()->create_user();
         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
@@ -1468,7 +1597,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
 
         $request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
 
-        $purposes->course->set('protected', 0)->save();
+        $purposes->course->purpose->set('protected', 0)->save();
         api::add_request_contexts_with_status($collection, $request->get('id'), contextlist_context::STATUS_APPROVED);
 
         $requests = contextlist_context::get_records();
@@ -1482,7 +1611,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->resetAfterTest();
 
         $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
-        $purposes->course->set('protected', 1)->save();
+        $purposes->course->purpose->set('protected', 1)->save();
 
         $user = $this->getDataGenerator()->create_user();
         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]);
@@ -1508,7 +1637,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
             ]);
         $rcl->save();
 
-        $purposes->course->set('protected', 1)->save();
+        $purposes->course->purpose->set('protected', 1)->save();
         $collection = api::get_approved_contextlist_collection_for_request($request);
 
         $this->assertCount(1, $collection);
@@ -1524,7 +1653,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->resetAfterTest();
 
         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
-        $purposes->course->set('protected', 1)->save();
+        $purposes->course->purpose->set('protected', 1)->save();
 
         $user = $this->getDataGenerator()->create_user();
         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
@@ -1550,7 +1679,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
             ]);
         $rcl->save();
 
-        $purposes->course->set('protected', 1)->save();
+        $purposes->course->purpose->set('protected', 1)->save();
         $collection = api::get_approved_contextlist_collection_for_request($request);
 
         $this->assertCount(0, $collection);
@@ -1566,7 +1695,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->resetAfterTest();
 
         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
-        $purposes->course->set('protected', 1)->save();
+        $purposes->course->purpose->set('protected', 1)->save();
 
         $user = $this->getDataGenerator()->create_user();
         $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
@@ -1592,7 +1721,7 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
             ]);
         $rcl->save();
 
-        $purposes->course->set('protected', 0)->save();
+        $purposes->course->purpose->set('protected', 0)->save();
         $collection = api::get_approved_contextlist_collection_for_request($request);
 
         $this->assertCount(1, $collection);
@@ -1893,9 +2022,8 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
      *
      * @param   string  $retention
      * @param   int     $contextlevel
-     * @return  purpose
      */
-    protected function create_and_set_purpose_for_contextlevel(string $retention, int $contextlevel) : purpose {
+    protected function create_and_set_purpose_for_contextlevel(string $retention, int $contextlevel) {
         $purpose = new purpose(0, (object) [
             'name' => 'Test purpose ' . rand(1, 1000),
             'retentionperiod' => $retention,
@@ -1920,6 +2048,9 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
             set_config($purposevar, $purpose->get('id'), 'tool_dataprivacy');
         }
 
-        return $purpose;
+        return (object) [
+            'purpose' => $purpose,
+            'category' => $cat,
+        ];
     }
 }
index a6287d5..0bb5e75 100644 (file)
@@ -32,12 +32,11 @@ Feature: Manage data registry defaults
       | Purpose 2    | P5Y            |
     And I set the site category and purpose to "Site category" and "Site purpose"
 
+  # Setting a default for course categories should apply to everything beneath that category.
   Scenario: Set course category data registry defaults
-    Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
-    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    Given I navigate to "Users > Privacy and policies > Data registry" in site administration
     And I click on "Set defaults" "link"
     And I should see "Inherit"
-    And I should not see "Add a new module default"
     And I press "Edit"
     And I set the field "Category" to "Category 1"
     And I set the field "Purpose" to "Purpose 1"
@@ -47,27 +46,91 @@ Feature: Manage data registry defaults
     And I navigate to "Users > Privacy and policies > Data registry" in site administration
     And I click on "Science and technology" "link"
     And I wait until the page is ready
-    And the field "categoryid" matches value "Category 2"
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years"
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I should see "3 years"
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I should see "3 years"
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And I should see "3 years"
+
+  # When Setting a default for course categories, and overriding a specific category, only that category and its
+  # children will be overridden.
+  # If any child is a course category, it will get the default.
+  Scenario: Set course category data registry defaults with override
+    Given I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I press "Save changes"
+    And I should see "Category 1"
+    And I should see "Purpose 1"
+    And I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
+    When I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    Then the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years"
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    # Physics 101 is also a category, so it will get the category default.
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I should see "3 years"
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I should see "3 years"
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And I should see "3 years"
+
+  # When overriding a specific category, only that category and its children will be overridden.
+  Scenario: Set course category data registry defaults with override
+    Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
+    When I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    Then the field "categoryid" matches value "Category 2"
     And the field "purposeid" matches value "Purpose 2"
     And I should see "5 years"
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    # Physics 101 is also a category, so it will get the category default.
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I should see "5 years"
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I should see "5 years"
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And I should see "5 years"
 
+  # Resetting instances removes custom values.
   Scenario: Set course category data registry defaults with override
     Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
     And I navigate to "Users > Privacy and policies > Data registry" in site administration
     And I click on "Set defaults" "link"
-    And I should see "Inherit"
-    And I should not see "Add a new module default"
     And I press "Edit"
     And I set the field "Category" to "Category 1"
     And I set the field "Purpose" to "Purpose 1"
-    And I click on "Reset instances with custom values" "checkbox"
-    When I press "Save changes"
-    Then I should see "Category 1"
+    When I click on "Reset instances with custom values" "checkbox"
+    And I press "Save changes"
+    And I should see "Category 1"
     And I should see "Purpose 1"
     And I navigate to "Users > Privacy and policies > Data registry" in site administration
     And I click on "Science and technology" "link"
     And I wait until the page is ready
-    And the field "categoryid" matches value "Not set (use the default value)"
+    Then the field "categoryid" matches value "Not set (use the default value)"
     And the field "purposeid" matches value "Not set (use the default value)"
     And I should see "3 years"
 
@@ -94,6 +157,12 @@ Feature: Manage data registry defaults
     And the field "categoryid" matches value "Category 2"
     And the field "purposeid" matches value "Purpose 2"
     And I should see "5 years (after the course end date)"
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I should see "5 years"
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And I should see "5 years"
 
   Scenario: Set course data registry defaults with override
     Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
@@ -119,6 +188,12 @@ Feature: Manage data registry defaults
     And the field "categoryid" matches value "Not set (use the default value)"
     And the field "purposeid" matches value "Not set (use the default value)"
     And I should see "3 years (after the course end date)"
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I should see "3 years"
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And I should see "3 years"
 
   Scenario: Set module level data registry defaults
     Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
diff --git a/admin/tool/dataprivacy/tests/data_registry_test.php b/admin/tool/dataprivacy/tests/data_registry_test.php
new file mode 100644 (file)
index 0000000..b565f72
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the data_registry class.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \tool_dataprivacy\data_registry;
+
+/**
+ * Unit tests for the data_registry class.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_dataprivacy_dataregistry_testcase extends advanced_testcase {
+
+    /**
+     * Ensure that the get_effective_context_value only errors if provided an inappropriate element.
+     *
+     * This test is not great because we only test a limited set of values. This is a fault of the underlying API.
+     */
+    public function test_get_effective_context_value_invalid_element() {
+        $this->expectException(coding_exception::class);
+        data_registry::get_effective_context_value(\context_system::instance(), 'invalid');
+    }
+
+    /**
+     * Ensure that the get_effective_contextlevel_value only errors if provided an inappropriate element.
+     *
+     * This test is not great because we only test a limited set of values. This is a fault of the underlying API.
+     */
+    public function test_get_effective_contextlevel_value_invalid_element() {
+        $this->expectException(coding_exception::class);
+        data_registry::get_effective_contextlevel_value(\context_system::instance(), 'invalid');
+    }
+}