MDL-63658 core_favourites: add a user favourites repository
authorJake Dallimore <jake@moodle.com>
Mon, 17 Sep 2018 00:38:50 +0000 (08:38 +0800)
committerJake Dallimore <jake@moodle.com>
Thu, 18 Oct 2018 00:22:06 +0000 (08:22 +0800)
favourites/classes/local/crud_repository.php [new file with mode: 0644]
favourites/classes/local/favourites_repository.php [new file with mode: 0644]
favourites/classes/local/ifavourites_repository.php [new file with mode: 0644]
favourites/tests/repository_test.php [new file with mode: 0644]

diff --git a/favourites/classes/local/crud_repository.php b/favourites/classes/local/crud_repository.php
new file mode 100644 (file)
index 0000000..8dab4e0
--- /dev/null
@@ -0,0 +1,100 @@
+<?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/>.
+/**
+ * Contains the crud_repository interface.
+ *
+ * @package   core_favourites
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_favourites\local;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The crud_repository interface, defining the basic CRUD operations for any repository types within core_favourites.
+ */
+interface crud_repository {
+    /**
+     * Add one item to this repository.
+     *
+     * @param \stdClass $item the item to add.
+     * @return \stdClass the item which was added.
+     */
+    public function add(\stdClass $item) : \stdClass;
+
+    /**
+     * Add all the items in the list to this repository.
+     *
+     * @param array $items the list of items to add.
+     * @return array the list of items added to this repository.
+     */
+    public function add_all(array $items) : array;
+
+    /**
+     * Find an item in this repository based on its id.
+     *
+     * @param int $id the id of the item.
+     * @return \stdClass the item.
+     */
+    public function find(int $id) : \stdClass;
+
+    /**
+     * Find all items in this repository.
+     *
+     * @return array list of all items in this repository.
+     */
+    public function find_all() : array;
+
+    /**
+     * Find all items with attributes matching certain values.
+     *
+     * @param array $criteria the array of attribute/value pairs.
+     * @return array the list of items matching the criteria.
+     */
+    public function find_by(array $criteria) : array;
+
+    /**
+     * Check whether an item exists in this repository, based on its id.
+     *
+     * @param int $id the id to search for.
+     * @return bool true if the item could be found, false otherwise.
+     */
+    public function exists(int $id) : bool;
+
+    /**
+     * Return the total number of items in this repository.
+     *
+     * @return int the total number of items.
+     */
+    public function count() : int;
+
+    /**
+     * Update an item within this repository.
+     *
+     * @param \stdClass $item the item to update.
+     * @return \stdClass the updated item.
+     */
+    public function update(\stdClass $item) : \stdClass;
+
+    /**
+     * Delete an item by id.
+     *
+     * @param int $id the id of the item to delete.
+     * @return void
+     */
+    public function delete(int $id);
+}
diff --git a/favourites/classes/local/favourites_repository.php b/favourites/classes/local/favourites_repository.php
new file mode 100644 (file)
index 0000000..3e69f2c
--- /dev/null
@@ -0,0 +1,290 @@
+<?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/>.
+/**
+ * Contains the user_favourites_repository class, responsible for CRUD operations for user favourites.
+ *
+ * @package   core_favourites
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_favourites\local;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class favourites_repository.
+ *
+ * This class handles persistence of favourites. Favourites from all areas are supported by this repository.
+ *
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class favourites_repository implements ifavourites_repository {
+
+    /**
+     * @var string the name of the table which favourites are stored in.
+     */
+    protected $favouritetable = 'favourite';
+
+    /**
+     * The favourites_repository constructor.
+     */
+    public function __construct() {
+    }
+
+    /**
+     * Add a favourite to the repository.
+     *
+     * @param \stdClass $favourite the favourite to add.
+     * @return \stdClass the favourite which has been stored.
+     * @throws \dml_exception if any database errors are encountered.
+     * @throws \moodle_exception if the favourite has missing or invalid properties.
+     */
+    public function add(\stdClass $favourite) : \stdClass {
+        global $DB;
+        $this->validate($favourite);
+        $favourite = (array)$favourite;
+        $time = time();
+        $favourite['timecreated'] = $time;
+        $favourite['timemodified'] = $time;
+        $id = $DB->insert_record($this->favouritetable, $favourite);
+        return $this->find($id);
+    }
+
+    /**
+     * Add a collection of favourites to the repository.
+     *
+     * @param array $items the list of favourites to add.
+     * @return array the list of favourites which have been stored.
+     * @throws \dml_exception if any database errors are encountered.
+     * @throws \moodle_exception if any of the favourites have missing or invalid properties.
+     */
+    public function add_all(array $items) : array {
+        global $DB;
+        $time = time();
+        foreach ($items as $item) {
+            $this->validate($item);
+            $favourite = (array)$item;
+            $favourite['timecreated'] = $time;
+            $favourite['timemodified'] = $time;
+            $ids[] = $DB->insert_record($this->favouritetable, $favourite);
+        }
+        list($insql, $params) = $DB->get_in_or_equal($ids);
+        return $DB->get_records_select($this->favouritetable, "id $insql", $params);
+    }
+
+    /**
+     * Find a favourite by id.
+     *
+     * @param int $id the id of the favourite.
+     * @return \stdClass the favourite.
+     * @throws \dml_exception if any database errors are encountered.
+     */
+    public function find(int $id) : \stdClass {
+        global $DB;
+        return $DB->get_record($this->favouritetable, ['id' => $id], '*', MUST_EXIST);
+    }
+
+    /**
+     * Return all items matching the supplied criteria (a [key => value,..] list).
+     *
+     * @param array $criteria the list of key/value criteria pairs.
+     * @return array the list of favourites matching the criteria.
+     * @throws \dml_exception if any database errors are encountered.
+     */
+    public function find_by(array $criteria) : array {
+        global $DB;
+        return $DB->get_records($this->favouritetable, $criteria);
+    }
+
+    /**
+     * Return all items in this repository, as an array, indexed by id.
+     *
+     * @return array the list of all favourites stored within this repository.
+     * @throws \dml_exception if any database errors are encountered.
+     */
+    public function find_all() : array {
+        global $DB;
+        return $DB->get_records($this->favouritetable);
+    }
+
+    /**
+     * Find a specific favourite, based on the properties known to identify it.
+     *
+     * Used if we don't know its id.
+     *
+     * @param int $userid the id of the user to which the favourite belongs.
+     * @param string $component the frankenstyle component name.
+     * @param string $itemtype the type of the favourited item.
+     * @param int $itemid the id of the item which was favourited (not the favourite's id).
+     * @param int $contextid the contextid of the item which was favourited.
+     * @return \stdClass the favourite.
+     * @throws \dml_exception if any database errors are encountered or if the record could not be found.
+     */
+    public function find_favourite(int $userid, string $component, string $itemtype, int $itemid, int $contextid) : \stdClass {
+        global $DB;
+        // Favourites model: We know that only one favourite can exist based on these properties.
+        return $DB->get_record($this->favouritetable, [
+            'userid' => $userid,
+            'component' => $component,
+            'itemtype' => $itemtype,
+            'itemid' => $itemid,
+            'contextid' => $contextid
+        ], '*', MUST_EXIST);
+    }
+
+    /**
+     * Check whether a favourite exists in this repository, based on its id.
+     *
+     * @param int $id the id to search for.
+     * @return bool true if the favourite exists, false otherwise.
+     * @throws \dml_exception if any database errors are encountered.
+     */
+    public function exists(int $id) : bool {
+        global $DB;
+        return $DB->record_exists($this->favouritetable, ['id' => $id]);
+    }
+
+    /**
+     * Update a favourite.
+     *
+     * @param \stdClass $favourite the favourite to update.
+     * @return \stdClass the updated favourite.
+     * @throws \dml_exception if any database errors are encountered.
+     */
+    public function update(\stdClass $favourite) : \stdClass {
+        global $DB;
+        $time = time();
+        $favourite->timemodified = $time;
+        $DB->update_record($this->favouritetable, $favourite);
+        return $this->find($favourite->id);
+    }
+
+    /**
+     * Delete a favourite, by id.
+     *
+     * @param int $id the id of the favourite to delete.
+     * @throws \dml_exception if any database errors are encountered.
+     */
+    public function delete(int $id) {
+        global $DB;
+        $DB->delete_records($this->favouritetable, ['id' => $id]);
+    }
+
+    /**
+     * Return the total number of favourites in this repository.
+     *
+     * @return int the total number of items.
+     * @throws \dml_exception if any database errors are encountered.
+     */
+    public function count() : int {
+        global $DB;
+        return $DB->count_records($this->favouritetable);
+    }
+
+    /**
+     * Check for the existence of a favourite item in the specified area.
+     *
+     * A favourite item is identified by the itemid/contextid pair.
+     * An area is identified by the component/itemtype pair.
+     *
+     * @param int $userid the id of user to whom the favourite belongs.
+     * @param string $component the frankenstyle component name.
+     * @param string $itemtype the type of the favourited item.
+     * @param int $itemid the id of the item which was favourited (not the favourite's id).
+     * @param int $contextid the contextid of the item which was favourited.
+     * @return bool true if the favourited item exists, false otherwise.
+     * @throws \dml_exception if any database errors are encountered.
+     */
+    public function exists_by_area(int $userid, string $component, string $itemtype, int $itemid, int $contextid) : bool {
+        global $DB;
+        return $DB->record_exists($this->favouritetable,
+            [
+                'userid' => $userid,
+                'component' => $component,
+                'itemtype' => $itemtype,
+                'itemid' => $itemid,
+                'contextid' => $contextid
+            ]
+        );
+    }
+
+    /**
+     * Delete all favourites within the component/itemtype.
+     *
+     * @param int $userid the id of the user to whom the favourite belongs.
+     * @param string $component the frankenstyle component name.
+     * @param string $itemtype the type of the favourited item.
+     * @throws \dml_exception if any database errors are encountered.
+     */
+    public function delete_by_area(int $userid, string $component, string $itemtype) {
+        global $DB;
+        $DB->delete_records($this->favouritetable,
+            [
+                'userid' => $userid,
+                'component' => $component,
+                'itemtype' => $itemtype
+            ]
+        );
+    }
+
+    /**
+     * Return the number of user favourites matching the specified criteria.
+     *
+     * @param array $criteria the list of key/value criteria pairs.
+     * @return int the number of favourites matching the criteria.
+     * @throws \dml_exception if any database errors are encountered.
+     */
+    public function count_by(array $criteria) : int {
+        global $DB;
+        return $DB->count_records($this->favouritetable, $criteria);
+    }
+
+    /**
+     * Basic validation, confirming we have the minimum field set needed to save a record to the store.
+     *
+     * @param \stdClass $favourite the favourite record to validate.
+     * @throws \moodle_exception if the supplied favourite has missing or unsupported fields.
+     */
+    protected function validate(\stdClass $favourite) {
+
+        $favourite = (array)$favourite;
+
+        // The allowed fields, and whether or not each is required.
+        // The timecreated field is generated during create/update, and cannot be specified either.
+        $allowedfields = [
+            'userid' => true,
+            'component' => true,
+            'itemtype' => true,
+            'itemid' => true,
+            'contextid' => true,
+            'ordering' => false
+        ];
+
+        $requiredfields = array_filter($allowedfields, function($field) {
+            return $field;
+        });
+
+        if ($missingfields = array_keys(array_diff_key($requiredfields, $favourite))) {
+            throw new \moodle_exception("Missing object property(s) '" . join(', ', $missingfields) . "'.");
+        }
+
+        // If the record contains fields we don't allow, throw an exception.
+        if ($unsupportedfields = array_keys(array_diff_key($favourite, $allowedfields))) {
+            throw new \moodle_exception("Unexpected object property(s) '" . join(', ', $unsupportedfields) . "'.");
+        }
+    }
+}
diff --git a/favourites/classes/local/ifavourites_repository.php b/favourites/classes/local/ifavourites_repository.php
new file mode 100644 (file)
index 0000000..493934b
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+/**
+ * Contains the favourites_repository interface.
+ *
+ * @package   core_favourites
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_favourites\local;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The favourites_repository interface, defining additional operations useful to favourites type repositories.
+ *
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface ifavourites_repository extends crud_repository {
+    /**
+     * Find a single favourite, based on it's unique identifiers.
+     *
+     * @param int $userid the id of the user to which the favourite belongs.
+     * @param string $component the frankenstyle component name.
+     * @param string $itemtype the type of the favourited item.
+     * @param int $itemid the id of the item which was favourited (not the favourite's id).
+     * @param int $contextid the contextid of the item which was favourited.
+     * @return \stdClass the favourite.
+     */
+    public function find_favourite(int $userid, string $component, string $itemtype, int $itemid, int $contextid) : \stdClass;
+}
diff --git a/favourites/tests/repository_test.php b/favourites/tests/repository_test.php
new file mode 100644 (file)
index 0000000..f7c713d
--- /dev/null
@@ -0,0 +1,439 @@
+<?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/>.
+
+/**
+ * Testing the repository objects within core_favourites.
+ *
+ * @package    core_favourites
+ * @category   test
+ * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_favourites\local\favourites_repository;
+
+/**
+ * Test class covering the favourites_repository.
+ *
+ * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class favourites_repository_testcase extends advanced_testcase {
+
+    public function setUp() {
+        $this->resetAfterTest();
+    }
+
+    // Basic setup stuff to be reused in most tests.
+    protected function setup_users_and_courses() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user1context = \context_user::instance($user1->id);
+        $user2 = self::getDataGenerator()->create_user();
+        $user2context = \context_user::instance($user2->id);
+        $course1 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course();
+        $course1context = context_course::instance($course1->id);
+        $course2context = context_course::instance($course2->id);
+        return [$user1context, $user2context, $course1context, $course2context];
+    }
+
+    /**
+     * Verify the basic create operation can create records, and is validated.
+     */
+    public function test_add() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Create a favourites repository and favourite a course.
+        $favouritesrepo = new favourites_repository($user1context);
+
+        $favcourse = (object)[
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid,
+            'contextid' => $course1context->id,
+        ];
+        $timenow = time(); // Reference only, to check that the created item has a time equal to or greater than this.
+        $favourite = $favouritesrepo->add($favcourse);
+
+        // Verify we get the record back.
+        $this->assertInstanceOf(\stdClass::class, $favourite);
+        $this->assertEquals('core_course', $favourite->component);
+        $this->assertEquals('course', $favourite->itemtype);
+
+        // Verify the returned object has additional properties, created as part of the add.
+        $this->assertObjectHasAttribute('ordering', $favourite);
+        $this->assertObjectHasAttribute('timecreated', $favourite);
+        $this->assertGreaterThanOrEqual($timenow, $favourite->timecreated);
+
+        // Try to save the same record again and confirm the store throws an exception.
+        $this->expectException('dml_write_exception');
+        $favouritesrepo->add($favcourse);
+    }
+
+    /**
+     * Tests that malformed favourites cannot be saved.
+     */
+    public function test_add_malformed_favourite() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Create a favourites repository and favourite a course.
+        $favouritesrepo = new favourites_repository($user1context);
+
+        $favcourse = (object)[
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid,
+            'contextid' => $course1context->id,
+            'anotherfield' => 'cat'
+        ];
+
+        $this->expectException('moodle_exception');
+        $favouritesrepo->add($favcourse);
+    }
+
+    /**
+     * Tests that incomplete favourites cannot be saved.
+     */
+    public function test_add_incomplete_favourite() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Create a favourites repository and favourite a course.
+        $favouritesrepo = new favourites_repository($user1context);
+
+        $favcourse = (object)[
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid
+        ];
+
+        $this->expectException('moodle_exception');
+        $favouritesrepo->add($favcourse);
+    }
+
+    public function test_add_all_basic() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Create a favourites repository and favourite several courses.
+        $favouritesrepo = new favourites_repository($user1context);
+        $favcourses = [];
+
+        $favcourses[] = (object)[
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid,
+            'contextid' => $course1context->id,
+        ];
+        $favcourses[] = (object)[
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course2context->instanceid,
+            'contextid' => $course2context->id,
+        ];
+        $timenow = time(); // Reference only, to check that the created item has a time equal to or greater than this.
+        $favourites = $favouritesrepo->add_all($favcourses);
+
+        $this->assertInternalType('array', $favourites);
+        $this->assertCount(2, $favourites);
+        foreach ($favourites as $favourite) {
+            // Verify we get the record back.
+            $this->assertEquals('core_course', $favourite->component);
+            $this->assertEquals('course', $favourite->itemtype);
+
+            // Verify the returned object has additional properties, created as part of the add.
+            $this->assertObjectHasAttribute('ordering', $favourite);
+            $this->assertObjectHasAttribute('timecreated', $favourite);
+            $this->assertGreaterThanOrEqual($timenow, $favourite->timecreated);
+        }
+
+        // Try to save the same record again and confirm the store throws an exception.
+        $this->expectException('dml_write_exception');
+        $favouritesrepo->add_all($favcourses);
+    }
+
+    /**
+     * Tests reading from the repository by instance id.
+     */
+    public function test_find() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Create a favourites repository and favourite a course.
+        $favouritesrepo = new favourites_repository($user1context);
+        $favourite = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid,
+            'contextid' => $course1context->id
+        ];
+        $favourite = $favouritesrepo->add($favourite);
+
+        // Now, from the repo, get the single favourite we just created, by id.
+        $userfavourite = $favouritesrepo->find($favourite->id);
+        $this->assertInstanceOf(\stdClass::class, $userfavourite);
+        $this->assertObjectHasAttribute('timecreated', $userfavourite);
+
+        // Try to get a favourite we know doesn't exist.
+        // We expect an exception in this case.
+        $this->expectException(dml_exception::class);
+        $favouritesrepo->find(1);
+    }
+
+    /**
+     * Test verifying that find_all() returns all favourites, or an empty array.
+     */
+    public function test_find_all() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        $favouritesrepo = new favourites_repository($user1context);
+
+        // Verify that for an empty repository, find_all returns an empty array.
+        $this->assertEquals([], $favouritesrepo->find_all());
+
+        // Save a favourite for 2 courses, in different areas.
+        $favourite = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid,
+            'contextid' => $course1context->id
+        ];
+        $favourite2 = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'anothertype',
+            'itemid' => $course2context->instanceid,
+            'contextid' => $course2context->id
+        ];
+        $favouritesrepo->add($favourite);
+        $favouritesrepo->add($favourite2);
+
+        // Verify that find_all returns both of our favourites.
+        $favourites = $favouritesrepo->find_all();
+        $this->assertCount(2, $favourites);
+        foreach ($favourites as $fav) {
+            $this->assertObjectHasAttribute('id', $fav);
+            $this->assertObjectHasAttribute('timecreated', $fav);
+        }
+    }
+
+    /**
+     * Test retrieval of a user's favourites for a given criteria, in this case, area.
+     */
+    public function test_find_by() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Create a favourites repository and favourite a course.
+        $favouritesrepo = new favourites_repository($user1context);
+        $favourite = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid,
+            'contextid' => $course1context->id
+        ];
+        $favouritesrepo->add($favourite);
+
+        // From the repo, get the list of favourites for the 'core_course/course' area.
+        $userfavourites = $favouritesrepo->find_by(['component' => 'core_course', 'itemtype' => 'course']);
+        $this->assertInternalType('array', $userfavourites);
+        $this->assertCount(1, $userfavourites);
+
+        // Try to get a list of favourites for a non-existent area.
+        $userfavourites = $favouritesrepo->find_by(['component' => 'core_cannibalism', 'itemtype' => 'course']);
+        $this->assertInternalType('array', $userfavourites);
+        $this->assertCount(0, $userfavourites);
+    }
+
+    /**
+     * Test the count_by() method.
+     */
+    public function test_count_by() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Create a favourites repository and add 2 favourites in different areas.
+        $favouritesrepo = new favourites_repository($user1context);
+        $favourite = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid,
+            'contextid' => $course1context->id
+        ];
+        $favourite2 = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'anothertype',
+            'itemid' => $course2context->instanceid,
+            'contextid' => $course2context->id
+        ];
+        $favouritesrepo->add($favourite);
+        $favouritesrepo->add($favourite2);
+
+        // Verify counts can be restricted by criteria.
+        $this->assertEquals(1, $favouritesrepo->count_by(['userid' => $user1context->instanceid, 'component' => 'core_course',
+                'itemtype' => 'course']));
+        $this->assertEquals(1, $favouritesrepo->count_by(['userid' => $user1context->instanceid, 'component' => 'core_course',
+            'itemtype' => 'anothertype']));
+        $this->assertEquals(0, $favouritesrepo->count_by(['userid' => $user1context->instanceid, 'component' => 'core_course',
+            'itemtype' => 'nonexistenttype']));
+    }
+
+    public function test_exists() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Create a favourites repository and favourite a course.
+        $favouritesrepo = new favourites_repository($user1context);
+        $favourite = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid,
+            'contextid' => $course1context->id
+        ];
+        $createdfavourite = $favouritesrepo->add($favourite);
+
+        // Verify the existence of the favourite in the repo.
+        $this->assertTrue($favouritesrepo->exists($createdfavourite->id));
+
+        // Verify exists returns false for non-existent favourite.
+        $this->assertFalse($favouritesrepo->exists(1));
+    }
+
+    public function test_exists_by_area() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Create a favourites repository and favourite two courses, in different areas.
+        $favouritesrepo = new favourites_repository($user1context);
+        $favourite = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid,
+            'contextid' => $course1context->id
+        ];
+        $favourite2 = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'anothertype',
+            'itemid' => $course2context->instanceid,
+            'contextid' => $course2context->id
+        ];
+        $favourite1 = $favouritesrepo->add($favourite);
+        $favourite2 = $favouritesrepo->add($favourite2);
+
+        // Verify the existence of the favourites.
+        $this->assertTrue($favouritesrepo->exists_by_area($user1context->instanceid, 'core_course', 'course', $favourite1->itemid,
+            $favourite1->contextid));
+        $this->assertTrue($favouritesrepo->exists_by_area($user1context->instanceid, 'core_course', 'anothertype',
+            $favourite2->itemid, $favourite2->contextid));
+
+        // Verify that we can't find a favourite from one area, in another.
+        $this->assertFalse($favouritesrepo->exists_by_area($user1context->instanceid, 'core_course', 'anothertype',
+            $favourite1->itemid, $favourite1->contextid));
+    }
+
+    /**
+     * Test the update() method, by simulating a user changing the ordering of a favourite.
+     */
+    public function test_update() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Create a favourites repository and favourite a course.
+        $favouritesrepo = new favourites_repository($user1context);
+        $favourite = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid,
+            'contextid' => $course1context->id
+        ];
+        $favourite1 = $favouritesrepo->add($favourite);
+
+        // Verify we can update the ordering for 2 favourites.
+        $favourite1->ordering = 1;
+        $favourite1 = $favouritesrepo->update($favourite1);
+        $this->assertInstanceOf(stdClass::class, $favourite1);
+        $this->assertAttributeEquals('1', 'ordering', $favourite1);
+    }
+
+    public function test_delete() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Create a favourites repository and favourite a course.
+        $favouritesrepo = new favourites_repository($user1context);
+        $favourite = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid,
+            'contextid' => $course1context->id
+        ];
+        $favourite = $favouritesrepo->add($favourite);
+
+        // Verify the existence of the favourite in the repo.
+        $this->assertTrue($favouritesrepo->exists($favourite->id));
+
+        // Now, delete the favourite and confirm it's not retrievable.
+        $favouritesrepo->delete($favourite->id);
+        $this->assertFalse($favouritesrepo->exists($favourite->id));
+    }
+
+    public function test_delete_by_area() {
+        list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
+
+        // Create a favourites repository and favourite two courses, in different areas.
+        $favouritesrepo = new favourites_repository($user1context);
+        $favourite = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'course',
+            'itemid' => $course1context->instanceid,
+            'contextid' => $course1context->id
+        ];
+        $favourite2 = (object) [
+            'userid' => $user1context->instanceid,
+            'component' => 'core_course',
+            'itemtype' => 'anothertype',
+            'itemid' => $course2context->instanceid,
+            'contextid' => $course2context->id
+        ];
+        $favourite1 = $favouritesrepo->add($favourite);
+        $favourite2 = $favouritesrepo->add($favourite2);
+
+        // Verify we have 2 items in the repo.
+        $this->assertEquals(2, $favouritesrepo->count());
+
+        // Try to delete by a non-existent area, and confirm it doesn't remove anything.
+        $favouritesrepo->delete_by_area($user1context->instanceid, 'core_course', 'donaldduck');
+        $this->assertEquals(2, $favouritesrepo->count());
+
+        // Try to delete by a non-existent area, and confirm it doesn't remove anything.
+        $favouritesrepo->delete_by_area($user1context->instanceid, 'core_course', 'cat');
+        $this->assertEquals(2, $favouritesrepo->count());
+
+        // Delete by area, and confirm we have one record left, from the 'core_course/anothertype' area.
+        $favouritesrepo->delete_by_area($user1context->instanceid, 'core_course', 'course');
+        $this->assertEquals(1, $favouritesrepo->count());
+        $this->assertFalse($favouritesrepo->exists($favourite1->id));
+        $this->assertTrue($favouritesrepo->exists($favourite2->id));
+    }
+}