Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Unit tests for badges
  19   *
  20   * @package    core
  21   * @subpackage badges
  22   * @copyright  2013 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  global $CFG;
  30  require_once($CFG->libdir . '/badgeslib.php');
  31  require_once($CFG->dirroot . '/badges/lib.php');
  32  
  33  use core_badges\helper;
  34  use core\task\manager;
  35  
  36  class badgeslib_test extends advanced_testcase {
  37      protected $badgeid;
  38      protected $course;
  39      protected $user;
  40      protected $module;
  41      protected $coursebadge;
  42      protected $assertion;
  43  
  44      /** @var $assertion2 to define json format for Open badge version 2 */
  45      protected $assertion2;
  46  
  47      protected function setUp(): void {
  48          global $DB, $CFG;
  49          $this->resetAfterTest(true);
  50          $CFG->enablecompletion = true;
  51          $user = $this->getDataGenerator()->create_user();
  52          $fordb = new stdClass();
  53          $fordb->id = null;
  54          $fordb->name = "Test badge with 'apostrophe' and other friends (<>&@#)";
  55          $fordb->description = "Testing badges";
  56          $fordb->timecreated = time();
  57          $fordb->timemodified = time();
  58          $fordb->usercreated = $user->id;
  59          $fordb->usermodified = $user->id;
  60          $fordb->issuername = "Test issuer";
  61          $fordb->issuerurl = "http://issuer-url.domain.co.nz";
  62          $fordb->issuercontact = "issuer@example.com";
  63          $fordb->expiredate = null;
  64          $fordb->expireperiod = null;
  65          $fordb->type = BADGE_TYPE_SITE;
  66          $fordb->version = 1;
  67          $fordb->language = 'en';
  68          $fordb->courseid = null;
  69          $fordb->messagesubject = "Test message subject";
  70          $fordb->message = "Test message body";
  71          $fordb->attachment = 1;
  72          $fordb->notification = 0;
  73          $fordb->imageauthorname = "Image Author 1";
  74          $fordb->imageauthoremail = "author@example.com";
  75          $fordb->imageauthorurl = "http://author-url.example.com";
  76          $fordb->imagecaption = "Test caption image";
  77          $fordb->status = BADGE_STATUS_INACTIVE;
  78  
  79          $this->badgeid = $DB->insert_record('badge', $fordb, true);
  80  
  81          // Set the default Issuer (because OBv2 needs them).
  82          set_config('badges_defaultissuername', $fordb->issuername);
  83          set_config('badges_defaultissuercontact', $fordb->issuercontact);
  84  
  85          // Create a course with activity and auto completion tracking.
  86          $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
  87          $this->user = $this->getDataGenerator()->create_user();
  88          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
  89          $this->assertNotEmpty($studentrole);
  90  
  91          // Get manual enrolment plugin and enrol user.
  92          require_once($CFG->dirroot.'/enrol/manual/locallib.php');
  93          $manplugin = enrol_get_plugin('manual');
  94          $maninstance = $DB->get_record('enrol', array('courseid' => $this->course->id, 'enrol' => 'manual'), '*', MUST_EXIST);
  95          $manplugin->enrol_user($maninstance, $this->user->id, $studentrole->id);
  96          $this->assertEquals(1, $DB->count_records('user_enrolments'));
  97          $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
  98          $this->module = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
  99  
 100          // Build badge and criteria.
 101          $fordb->type = BADGE_TYPE_COURSE;
 102          $fordb->courseid = $this->course->id;
 103          $fordb->status = BADGE_STATUS_ACTIVE;
 104          $this->coursebadge = $DB->insert_record('badge', $fordb, true);
 105  
 106          // Insert Endorsement.
 107          $endorsement = new stdClass();
 108          $endorsement->badgeid = $this->coursebadge;
 109          $endorsement->issuername = "Issuer 123";
 110          $endorsement->issueremail = "issuer123@email.com";
 111          $endorsement->issuerurl = "https://example.org/issuer-123";
 112          $endorsement->dateissued = 1524567747;
 113          $endorsement->claimid = "https://example.org/robotics-badge.json";
 114          $endorsement->claimcomment = "Test endorser comment";
 115          $DB->insert_record('badge_endorsement', $endorsement, true);
 116  
 117          // Insert related badges.
 118          $badge = new badge($this->coursebadge);
 119          $clonedid = $badge->make_clone();
 120          $badgeclone = new badge($clonedid);
 121          $badgeclone->status = BADGE_STATUS_ACTIVE;
 122          $badgeclone->save();
 123  
 124          $relatebadge = new stdClass();
 125          $relatebadge->badgeid = $this->coursebadge;
 126          $relatebadge->relatedbadgeid = $clonedid;
 127          $relatebadge->relatedid = $DB->insert_record('badge_related', $relatebadge, true);
 128  
 129          // Insert a aligment.
 130          $alignment = new stdClass();
 131          $alignment->badgeid = $this->coursebadge;
 132          $alignment->targetname = 'CCSS.ELA-Literacy.RST.11-12.3';
 133          $alignment->targeturl = 'http://www.corestandards.org/ELA-Literacy/RST/11-12/3';
 134          $alignment->targetdescription = 'Test target description';
 135          $alignment->targetframework = 'CCSS.RST.11-12.3';
 136          $alignment->targetcode = 'CCSS.RST.11-12.3';
 137          $DB->insert_record('badge_alignment', $alignment, true);
 138  
 139          $this->assertion = new stdClass();
 140          $this->assertion->badge = '{"uid":"%s","recipient":{"identity":"%s","type":"email","hashed":true,"salt":"%s"},"badge":"%s","verify":{"type":"hosted","url":"%s"},"issuedOn":"%d","evidence":"%s"}';
 141          $this->assertion->class = '{"name":"%s","description":"%s","image":"%s","criteria":"%s","issuer":"%s"}';
 142          $this->assertion->issuer = '{"name":"%s","url":"%s","email":"%s"}';
 143          // Format JSON-LD for Openbadge specification version 2.0.
 144          $this->assertion2 = new stdClass();
 145          $this->assertion2->badge = '{"recipient":{"identity":"%s","type":"email","hashed":true,"salt":"%s"},' .
 146              '"badge":{"name":"%s","description":"%s","image":"%s",' .
 147              '"criteria":{"id":"%s","narrative":"%s"},"issuer":{"name":"%s","url":"%s","email":"%s",' .
 148              '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"},' .
 149              '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' .
 150              '"@language":"en","related":[{"id":"%s","version":"%s","@language":"%s"}],"endorsement":"%s",' .
 151              '"alignments":[{"targetName":"%s","targetUrl":"%s","targetDescription":"%s","targetFramework":"%s",' .
 152              '"targetCode":"%s"}]},"verify":{"type":"hosted","url":"%s"},"issuedOn":"%s","evidence":"%s",' .
 153              '"@context":"https:\/\/w3id.org\/openbadges\/v2","type":"Assertion","id":"%s"}';
 154  
 155          $this->assertion2->class = '{"name":"%s","description":"%s","image":"%s",' .
 156              '"criteria":{"id":"%s","narrative":"%s"},"issuer":{"name":"%s","url":"%s","email":"%s",' .
 157              '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"},' .
 158              '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' .
 159              '"@language":"%s","related":[{"id":"%s","version":"%s","@language":"%s"}],"endorsement":"%s",' .
 160              '"alignments":[{"targetName":"%s","targetUrl":"%s","targetDescription":"%s","targetFramework":"%s",' .
 161              '"targetCode":"%s"}]}';
 162          $this->assertion2->issuer = '{"name":"%s","url":"%s","email":"%s",' .
 163              '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"}';
 164      }
 165  
 166      public function test_create_badge() {
 167          $badge = new badge($this->badgeid);
 168  
 169          $this->assertInstanceOf('badge', $badge);
 170          $this->assertEquals($this->badgeid, $badge->id);
 171      }
 172  
 173      public function test_clone_badge() {
 174          $badge = new badge($this->badgeid);
 175          $newid = $badge->make_clone();
 176          $clonedbadge = new badge($newid);
 177  
 178          $this->assertEquals($badge->description, $clonedbadge->description);
 179          $this->assertEquals($badge->issuercontact, $clonedbadge->issuercontact);
 180          $this->assertEquals($badge->issuername, $clonedbadge->issuername);
 181          $this->assertEquals($badge->issuercontact, $clonedbadge->issuercontact);
 182          $this->assertEquals($badge->issuerurl, $clonedbadge->issuerurl);
 183          $this->assertEquals($badge->expiredate, $clonedbadge->expiredate);
 184          $this->assertEquals($badge->expireperiod, $clonedbadge->expireperiod);
 185          $this->assertEquals($badge->type, $clonedbadge->type);
 186          $this->assertEquals($badge->courseid, $clonedbadge->courseid);
 187          $this->assertEquals($badge->message, $clonedbadge->message);
 188          $this->assertEquals($badge->messagesubject, $clonedbadge->messagesubject);
 189          $this->assertEquals($badge->attachment, $clonedbadge->attachment);
 190          $this->assertEquals($badge->notification, $clonedbadge->notification);
 191          $this->assertEquals($badge->version, $clonedbadge->version);
 192          $this->assertEquals($badge->language, $clonedbadge->language);
 193          $this->assertEquals($badge->imagecaption, $clonedbadge->imagecaption);
 194          $this->assertEquals($badge->imageauthorname, $clonedbadge->imageauthorname);
 195          $this->assertEquals($badge->imageauthoremail, $clonedbadge->imageauthoremail);
 196          $this->assertEquals($badge->imageauthorurl, $clonedbadge->imageauthorurl);
 197      }
 198  
 199      public function test_badge_status() {
 200          $badge = new badge($this->badgeid);
 201          $old_status = $badge->status;
 202          $badge->set_status(BADGE_STATUS_ACTIVE);
 203          $this->assertNotEquals($old_status, $badge->status);
 204          $this->assertEquals(BADGE_STATUS_ACTIVE, $badge->status);
 205      }
 206  
 207      public function test_delete_badge() {
 208          $badge = new badge($this->badgeid);
 209          $badge->delete();
 210          // We don't actually delete badges. We archive them.
 211          $this->assertEquals(BADGE_STATUS_ARCHIVED, $badge->status);
 212      }
 213  
 214      /**
 215       * Really delete the badge.
 216       */
 217      public function test_delete_badge_for_real() {
 218          global $DB;
 219  
 220          $badge = new badge($this->badgeid);
 221  
 222          $newid1 = $badge->make_clone();
 223          $newid2 = $badge->make_clone();
 224          $newid3 = $badge->make_clone();
 225  
 226          // Insert related badges to badge 1.
 227          $badge->add_related_badges([$newid1, $newid2, $newid3]);
 228  
 229          // Another badge.
 230          $badge2 = new badge($newid2);
 231          // Make badge 1 related for badge 2.
 232          $badge2->add_related_badges([$this->badgeid]);
 233  
 234          // Confirm that the records about this badge about its relations have been removed as well.
 235          $relatedsql = 'badgeid = :badgeid OR relatedbadgeid = :relatedbadgeid';
 236          $relatedparams = array(
 237              'badgeid' => $this->badgeid,
 238              'relatedbadgeid' => $this->badgeid
 239          );
 240          // Badge 1 has 4 related records. 3 where it's the badgeid, 1 where it's the relatedbadgeid.
 241          $this->assertEquals(4, $DB->count_records_select('badge_related', $relatedsql, $relatedparams));
 242  
 243          // Delete the badge for real.
 244          $badge->delete(false);
 245  
 246          // Confirm that the badge itself has been removed.
 247          $this->assertFalse($DB->record_exists('badge', ['id' => $this->badgeid]));
 248  
 249          // Confirm that the records about this badge about its relations have been removed as well.
 250          $this->assertFalse($DB->record_exists_select('badge_related', $relatedsql, $relatedparams));
 251      }
 252  
 253      public function test_create_badge_criteria() {
 254          $badge = new badge($this->badgeid);
 255          $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
 256          $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL));
 257  
 258          $this->assertCount(1, $badge->get_criteria());
 259  
 260          $criteria_profile = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
 261          $params = array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address');
 262          $criteria_profile->save($params);
 263  
 264          $this->assertCount(2, $badge->get_criteria());
 265      }
 266  
 267      public function test_add_badge_criteria_description() {
 268          $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $this->badgeid));
 269          $criteriaoverall->save(array(
 270                  'agg' => BADGE_CRITERIA_AGGREGATION_ALL,
 271                  'description' => 'Overall description',
 272                  'descriptionformat' => FORMAT_HTML
 273          ));
 274  
 275          $criteriaprofile = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $this->badgeid));
 276          $params = array(
 277                  'agg' => BADGE_CRITERIA_AGGREGATION_ALL,
 278                  'field_address' => 'address',
 279                  'description' => 'Description',
 280                  'descriptionformat' => FORMAT_HTML
 281          );
 282          $criteriaprofile->save($params);
 283  
 284          $badge = new badge($this->badgeid);
 285          $this->assertEquals('Overall description', $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->description);
 286          $this->assertEquals('Description', $badge->criteria[BADGE_CRITERIA_TYPE_PROFILE]->description);
 287      }
 288  
 289      public function test_delete_badge_criteria() {
 290          $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $this->badgeid));
 291          $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL));
 292          $badge = new badge($this->badgeid);
 293  
 294          $this->assertInstanceOf('award_criteria_overall', $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]);
 295  
 296          $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->delete();
 297          $this->assertEmpty($badge->get_criteria());
 298      }
 299  
 300      public function test_badge_awards() {
 301          global $DB;
 302          $this->preventResetByRollback(); // Messaging is not compatible with transactions.
 303          $badge = new badge($this->badgeid);
 304          $user1 = $this->getDataGenerator()->create_user();
 305  
 306          $sink = $this->redirectMessages();
 307  
 308          $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
 309          set_user_preference('message_provider_moodle_badgerecipientnotice_enabled', 'email', $user1);
 310  
 311          $badge->issue($user1->id, false);
 312          $this->assertDebuggingCalled(); // Expect debugging while baking a badge via phpunit.
 313          $this->assertTrue($badge->is_issued($user1->id));
 314  
 315          $messages = $sink->get_messages();
 316          $sink->close();
 317          $this->assertCount(1, $messages);
 318          $message = array_pop($messages);
 319          // Check we have the expected data.
 320          $customdata = json_decode($message->customdata);
 321          $this->assertObjectHasAttribute('notificationiconurl', $customdata);
 322          $this->assertObjectHasAttribute('hash', $customdata);
 323  
 324          $user2 = $this->getDataGenerator()->create_user();
 325          $badge->issue($user2->id, true);
 326          $this->assertTrue($badge->is_issued($user2->id));
 327  
 328          $this->assertCount(2, $badge->get_awards());
 329      }
 330  
 331      /**
 332       * Test the {@link badges_get_user_badges()} function in lib/badgeslib.php
 333       */
 334      public function test_badges_get_user_badges() {
 335          global $DB;
 336  
 337          // Messaging is not compatible with transactions.
 338          $this->preventResetByRollback();
 339  
 340          $badges = array();
 341          $user1 = $this->getDataGenerator()->create_user();
 342          $user2 = $this->getDataGenerator()->create_user();
 343  
 344          // Record the current time, we need to be precise about a couple of things.
 345          $now = time();
 346          // Create 11 badges with which to test.
 347          for ($i = 1; $i <= 11; $i++) {
 348              // Mock up a badge.
 349              $badge = new stdClass();
 350              $badge->id = null;
 351              $badge->name = "Test badge $i";
 352              $badge->description = "Testing badges $i";
 353              $badge->timecreated = $now - 12;
 354              $badge->timemodified = $now - 12;
 355              $badge->usercreated = $user1->id;
 356              $badge->usermodified = $user1->id;
 357              $badge->issuername = "Test issuer";
 358              $badge->issuerurl = "http://issuer-url.domain.co.nz";
 359              $badge->issuercontact = "issuer@example.com";
 360              $badge->expiredate = null;
 361              $badge->expireperiod = null;
 362              $badge->type = BADGE_TYPE_SITE;
 363              $badge->courseid = null;
 364              $badge->messagesubject = "Test message subject for badge $i";
 365              $badge->message = "Test message body for badge $i";
 366              $badge->attachment = 1;
 367              $badge->notification = 0;
 368              $badge->status = BADGE_STATUS_INACTIVE;
 369              $badge->version = "Version $i";
 370              $badge->language = "en";
 371              $badge->imagecaption = "Image caption $i";
 372              $badge->imageauthorname = "Image author's name $i";
 373              $badge->imageauthoremail = "author$i@example.com";
 374              $badge->imageauthorname = "Image author's name $i";
 375  
 376              $badgeid = $DB->insert_record('badge', $badge, true);
 377              $badges[$badgeid] = new badge($badgeid);
 378              $badges[$badgeid]->issue($user2->id, true);
 379              // Check it all actually worked.
 380              $this->assertCount(1, $badges[$badgeid]->get_awards());
 381  
 382              // Hack the database to adjust the time each badge was issued.
 383              // The alternative to this is sleep which is a no-no in unit tests.
 384              $DB->set_field('badge_issued', 'dateissued', $now - 11 + $i, array('userid' => $user2->id, 'badgeid' => $badgeid));
 385          }
 386  
 387          // Make sure the first user has no badges.
 388          $result = badges_get_user_badges($user1->id);
 389          $this->assertIsArray($result);
 390          $this->assertCount(0, $result);
 391  
 392          // Check that the second user has the expected 11 badges.
 393          $result = badges_get_user_badges($user2->id);
 394          $this->assertCount(11, $result);
 395  
 396          // Test pagination.
 397          // Ordering is by time issued desc, so things will come out with the last awarded badge first.
 398          $result = badges_get_user_badges($user2->id, 0, 0, 4);
 399          $this->assertCount(4, $result);
 400          $lastbadgeissued = reset($result);
 401          $this->assertSame('Test badge 11', $lastbadgeissued->name);
 402          // Page 2. Expecting 4 results again.
 403          $result = badges_get_user_badges($user2->id, 0, 1, 4);
 404          $this->assertCount(4, $result);
 405          $lastbadgeissued = reset($result);
 406          $this->assertSame('Test badge 7', $lastbadgeissued->name);
 407          // Page 3. Expecting just three results here.
 408          $result = badges_get_user_badges($user2->id, 0, 2, 4);
 409          $this->assertCount(3, $result);
 410          $lastbadgeissued = reset($result);
 411          $this->assertSame('Test badge 3', $lastbadgeissued->name);
 412          // Page 4.... there is no page 4.
 413          $result = badges_get_user_badges($user2->id, 0, 3, 4);
 414          $this->assertCount(0, $result);
 415  
 416          // Test search.
 417          $result = badges_get_user_badges($user2->id, 0, 0, 0, 'badge 1');
 418          $this->assertCount(3, $result);
 419          $lastbadgeissued = reset($result);
 420          $this->assertSame('Test badge 11', $lastbadgeissued->name);
 421          // The term Totara doesn't appear anywhere in the badges.
 422          $result = badges_get_user_badges($user2->id, 0, 0, 0, 'Totara');
 423          $this->assertCount(0, $result);
 424  
 425          // Issue a user with a course badge and verify its returned based on if
 426          // coursebadges are enabled or disabled.
 427          $sitebadgeid = key($badges);
 428          $badges[$sitebadgeid]->issue($this->user->id, true);
 429  
 430          $badge = new stdClass();
 431          $badge->id = null;
 432          $badge->name = "Test course badge";
 433          $badge->description = "Testing course badge";
 434          $badge->timecreated = $now;
 435          $badge->timemodified = $now;
 436          $badge->usercreated = $user1->id;
 437          $badge->usermodified = $user1->id;
 438          $badge->issuername = "Test issuer";
 439          $badge->issuerurl = "http://issuer-url.domain.co.nz";
 440          $badge->issuercontact = "issuer@example.com";
 441          $badge->expiredate = null;
 442          $badge->expireperiod = null;
 443          $badge->type = BADGE_TYPE_COURSE;
 444          $badge->courseid = $this->course->id;
 445          $badge->messagesubject = "Test message subject for course badge";
 446          $badge->message = "Test message body for course badge";
 447          $badge->attachment = 1;
 448          $badge->notification = 0;
 449          $badge->status = BADGE_STATUS_ACTIVE;
 450          $badge->version = "Version $i";
 451          $badge->language = "en";
 452          $badge->imagecaption = "Image caption";
 453          $badge->imageauthorname = "Image author's name";
 454          $badge->imageauthoremail = "author@example.com";
 455          $badge->imageauthorname = "Image author's name";
 456  
 457          $badgeid = $DB->insert_record('badge', $badge, true);
 458          $badges[$badgeid] = new badge($badgeid);
 459          $badges[$badgeid]->issue($this->user->id, true);
 460  
 461          // With coursebadges off, we should only get the site badge.
 462          set_config('badges_allowcoursebadges', false);
 463          $result = badges_get_user_badges($this->user->id);
 464          $this->assertCount(1, $result);
 465  
 466          // With it on, we should get both.
 467          set_config('badges_allowcoursebadges', true);
 468          $result = badges_get_user_badges($this->user->id);
 469          $this->assertCount(2, $result);
 470  
 471      }
 472  
 473      public function data_for_message_from_template() {
 474          return array(
 475              array(
 476                  'This is a message with no variables',
 477                  array(), // no params
 478                  'This is a message with no variables'
 479              ),
 480              array(
 481                  'This is a message with %amissing% variables',
 482                  array(), // no params
 483                  'This is a message with %amissing% variables'
 484              ),
 485              array(
 486                  'This is a message with %one% variable',
 487                  array('one' => 'a single'),
 488                  'This is a message with a single variable'
 489              ),
 490              array(
 491                  'This is a message with %one% %two% %three% variables',
 492                  array('one' => 'more', 'two' => 'than', 'three' => 'one'),
 493                  'This is a message with more than one variables'
 494              ),
 495              array(
 496                  'This is a message with %three% %two% %one%',
 497                  array('one' => 'variables', 'two' => 'ordered', 'three' => 'randomly'),
 498                  'This is a message with randomly ordered variables'
 499              ),
 500              array(
 501                  'This is a message with %repeated% %one% %repeated% of variables',
 502                  array('one' => 'and', 'repeated' => 'lots'),
 503                  'This is a message with lots and lots of variables'
 504              ),
 505          );
 506      }
 507  
 508      /**
 509       * @dataProvider data_for_message_from_template
 510       */
 511      public function test_badge_message_from_template($message, $params, $result) {
 512          $this->assertEquals(badge_message_from_template($message, $params), $result);
 513      }
 514  
 515      /**
 516       * Test for working around the 61 tables join limit of mysql in award_criteria_activity in combination with the scheduled task.
 517       *
 518       * @covers \core_badges\badge::review_all_criteria
 519       */
 520      public function test_badge_activity_criteria_with_a_huge_number_of_coursemodules() {
 521          global $CFG;
 522          require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php');
 523  
 524          if (!PHPUNIT_LONGTEST) {
 525              $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
 526          }
 527  
 528          // Messaging is not compatible with transactions.
 529          $this->preventResetByRollback();
 530  
 531          // Create more than 61 modules to potentially trigger an mysql db error.
 532          $assigncount = 75;
 533          $assigns = [];
 534          for ($i = 1; $i <= $assigncount; $i++) {
 535              $assigns[] = $this->getDataGenerator()->create_module('assign', ['course' => $this->course->id], ['completion' => 1]);
 536          }
 537          $assigncmids = array_flip(array_map(fn ($assign) => $assign->cmid, $assigns));
 538          $criteriaactivityarray = array_fill_keys(array_keys($assigncmids), 1);
 539  
 540          // Set completion criteria.
 541          $criteriadata = (object) [
 542              'id' => $this->course->id,
 543              'criteria_activity' => $criteriaactivityarray,
 544          ];
 545          $criterion = new completion_criteria_activity();
 546          $criterion->update_config($criteriadata);
 547  
 548          $badge = new badge($this->coursebadge);
 549  
 550          $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
 551          $criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
 552          $criteriaactivity = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_ACTIVITY, 'badgeid' => $badge->id]);
 553  
 554          $modulescrit = ['agg' => BADGE_CRITERIA_AGGREGATION_ALL];
 555          foreach ($assigns as $assign) {
 556              $modulescrit['module_' . $assign->cmid] = $assign->cmid;
 557          }
 558          $criteriaactivity->save($modulescrit);
 559  
 560          // Take one assign to complete it later.
 561          $assigntemp = array_shift($assigns);
 562  
 563          // Mark the user to complete the modules.
 564          foreach ($assigns as $assign) {
 565              $cmassign = get_coursemodule_from_id('assign', $assign->cmid);
 566              $completion = new \completion_info($this->course);
 567              $completion->update_state($cmassign, COMPLETION_COMPLETE, $this->user->id);
 568          }
 569  
 570          // Run the scheduled task to issue the badge. But the badge should not be issued.
 571          ob_start();
 572          $task = manager::get_scheduled_task('core\task\badges_cron_task');
 573          $task->execute();
 574          ob_end_clean();
 575  
 576          $this->assertFalse($badge->is_issued($this->user->id));
 577  
 578          // Now complete the last uncompleted module.
 579          $cmassign = get_coursemodule_from_id('assign', $assigntemp->cmid);
 580          $completion = new \completion_info($this->course);
 581          $completion->update_state($cmassign, COMPLETION_COMPLETE, $this->user->id);
 582  
 583          // Run the scheduled task to issue the badge. Now the badge schould be issued.
 584          ob_start();
 585          $task = manager::get_scheduled_task('core\task\badges_cron_task');
 586          $task->execute();
 587          ob_end_clean();
 588  
 589          $this->assertDebuggingCalled('Error baking badge image!');
 590          $this->assertTrue($badge->is_issued($this->user->id));
 591      }
 592  
 593      /**
 594       * Test badges observer when course module completion event id fired.
 595       */
 596      public function test_badges_observer_course_module_criteria_review() {
 597          $this->preventResetByRollback(); // Messaging is not compatible with transactions.
 598          $badge = new badge($this->coursebadge);
 599          $this->assertFalse($badge->is_issued($this->user->id));
 600  
 601          $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
 602          $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
 603          $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_ACTIVITY, 'badgeid' => $badge->id));
 604          $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'module_'.$this->module->cmid => $this->module->cmid));
 605  
 606          // Assert the badge will not be issued to the user as is.
 607          $badge = new badge($this->coursebadge);
 608          $badge->review_all_criteria();
 609          $this->assertFalse($badge->is_issued($this->user->id));
 610  
 611          // Set completion for forum activity.
 612          $c = new completion_info($this->course);
 613          $activities = $c->get_activities();
 614          $this->assertEquals(1, count($activities));
 615          $this->assertTrue(isset($activities[$this->module->cmid]));
 616          $this->assertEquals($activities[$this->module->cmid]->name, $this->module->name);
 617  
 618          $current = $c->get_data($activities[$this->module->cmid], false, $this->user->id);
 619          $current->completionstate = COMPLETION_COMPLETE;
 620          $current->timemodified = time();
 621          $sink = $this->redirectEmails();
 622          $c->internal_set_data($activities[$this->module->cmid], $current);
 623          $this->assertCount(1, $sink->get_messages());
 624          $sink->close();
 625  
 626          // Check if badge is awarded.
 627          $this->assertDebuggingCalled('Error baking badge image!');
 628          $this->assertTrue($badge->is_issued($this->user->id));
 629      }
 630  
 631      /**
 632       * Test badges observer when course_completed event is fired.
 633       */
 634      public function test_badges_observer_course_criteria_review() {
 635          $this->preventResetByRollback(); // Messaging is not compatible with transactions.
 636          $badge = new badge($this->coursebadge);
 637          $this->assertFalse($badge->is_issued($this->user->id));
 638  
 639          $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
 640          $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
 641          $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_COURSE, 'badgeid' => $badge->id));
 642          $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'course_'.$this->course->id => $this->course->id));
 643  
 644          $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
 645  
 646          // Assert the badge will not be issued to the user as is.
 647          $badge = new badge($this->coursebadge);
 648          $badge->review_all_criteria();
 649          $this->assertFalse($badge->is_issued($this->user->id));
 650  
 651          // Mark course as complete.
 652          $sink = $this->redirectMessages();
 653          $ccompletion->mark_complete();
 654          // Two messages are generated: One for the course completed and the other one for the badge awarded.
 655          $messages = $sink->get_messages();
 656          $this->assertCount(2, $messages);
 657          $this->assertEquals('badgerecipientnotice', $messages[0]->eventtype);
 658          $this->assertEquals('coursecompleted', $messages[1]->eventtype);
 659          $sink->close();
 660  
 661          // Check if badge is awarded.
 662          $this->assertDebuggingCalled('Error baking badge image!');
 663          $this->assertTrue($badge->is_issued($this->user->id));
 664      }
 665  
 666      /**
 667       * Test badges observer when user_updated event is fired.
 668       */
 669      public function test_badges_observer_profile_criteria_review() {
 670          global $CFG, $DB;
 671          require_once($CFG->dirroot.'/user/profile/lib.php');
 672  
 673          // Add a custom field of textarea type.
 674          $customprofileid = $this->getDataGenerator()->create_custom_profile_field(array(
 675              'shortname' => 'newfield', 'name' => 'Description of new field',
 676              'datatype' => 'textarea'))->id;
 677  
 678          $this->preventResetByRollback(); // Messaging is not compatible with transactions.
 679          $badge = new badge($this->coursebadge);
 680  
 681          $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
 682          $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
 683          $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
 684          $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address',
 685              'field_department' => 'department', 'field_' . $customprofileid => $customprofileid));
 686  
 687          // Assert the badge will not be issued to the user as is.
 688          $badge = new badge($this->coursebadge);
 689          $badge->review_all_criteria();
 690          $this->assertFalse($badge->is_issued($this->user->id));
 691  
 692          // Set the required fields and make sure the badge got issued.
 693          $this->user->address = 'Test address';
 694          $this->user->department = 'sillywalks';
 695          $sink = $this->redirectEmails();
 696          profile_save_data((object)array('id' => $this->user->id, 'profile_field_newfield' => 'X'));
 697          user_update_user($this->user, false);
 698          $this->assertCount(1, $sink->get_messages());
 699          $sink->close();
 700          // Check if badge is awarded.
 701          $this->assertDebuggingCalled('Error baking badge image!');
 702          $this->assertTrue($badge->is_issued($this->user->id));
 703      }
 704  
 705      /**
 706       * Test badges observer when cohort_member_added event is fired and user required to belong to any cohort.
 707       *
 708       * @covers \award_criteria_cohort
 709       */
 710      public function test_badges_observer_any_cohort_criteria_review() {
 711          global $CFG;
 712  
 713          require_once("$CFG->dirroot/cohort/lib.php");
 714  
 715          $cohort1 = $this->getDataGenerator()->create_cohort();
 716          $cohort2 = $this->getDataGenerator()->create_cohort();
 717  
 718          $this->preventResetByRollback(); // Messaging is not compatible with transactions.
 719  
 720          $badge = new badge($this->badgeid);
 721          $this->assertFalse($badge->is_issued($this->user->id));
 722          $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
 723  
 724          // Set up the badge criteria.
 725          $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
 726          $criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
 727          $criteriaoverall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_COHORT, 'badgeid' => $badge->id));
 728          $criteriaoverall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY,
 729              'cohort_cohorts' => array('0' => $cohort1->id, '1' => $cohort2->id)));
 730          $badge->set_status(BADGE_STATUS_ACTIVE);
 731  
 732          // Reload it to contain criteria.
 733          $badge = new badge($this->badgeid);
 734          $this->assertFalse($badge->is_issued($this->user->id));
 735          $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
 736  
 737          // Add the user to the cohort.
 738          cohort_add_member($cohort2->id, $this->user->id);
 739          $this->assertDebuggingCalled();
 740  
 741          // Verify that the badge was awarded.
 742          $this->assertTrue($badge->is_issued($this->user->id));
 743          // As the badge has been awarded to user because core_badges_observer been called when the member has been added to the
 744          // cohort, there are no other users that can award this badge.
 745          $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
 746      }
 747  
 748      /**
 749       * Test badges observer when cohort_member_added event is fired and user required to belong to multiple (all) cohorts.
 750       *
 751       * @covers \award_criteria_cohort
 752       */
 753      public function test_badges_observer_all_cohort_criteria_review() {
 754          global $CFG;
 755  
 756          require_once("$CFG->dirroot/cohort/lib.php");
 757  
 758          $cohort1 = $this->getDataGenerator()->create_cohort();
 759          $cohort2 = $this->getDataGenerator()->create_cohort();
 760          $cohort3 = $this->getDataGenerator()->create_cohort();
 761  
 762          // Add user2 to cohort1 and cohort3.
 763          $user2 = $this->getDataGenerator()->create_user();
 764          cohort_add_member($cohort3->id, $user2->id);
 765          cohort_add_member($cohort1->id, $user2->id);
 766  
 767          // Add user3 to cohort1, cohort2 and cohort3.
 768          $user3 = $this->getDataGenerator()->create_user();
 769          cohort_add_member($cohort1->id, $user3->id);
 770          cohort_add_member($cohort2->id, $user3->id);
 771          cohort_add_member($cohort3->id, $user3->id);
 772  
 773          $this->preventResetByRollback(); // Messaging is not compatible with transactions.
 774  
 775          // Cohort criteria are used in site badges.
 776          $badge = new badge($this->badgeid);
 777  
 778          $this->assertFalse($badge->is_issued($this->user->id));
 779          $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
 780  
 781          // Set up the badge criteria.
 782          $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
 783          $criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
 784          $criteriaoverall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_COHORT, 'badgeid' => $badge->id));
 785          $criteriaoverall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL,
 786              'cohort_cohorts' => array('0' => $cohort1->id, '1' => $cohort2->id, '2' => $cohort3->id)));
 787          $badge->set_status(BADGE_STATUS_ACTIVE);
 788  
 789          // Reload it to contain criteria.
 790          $badge = new badge($this->badgeid);
 791  
 792          // Verify that the badge was not awarded yet (ALL cohorts are needed and review_all_criteria has to be called).
 793          $this->assertFalse($badge->is_issued($this->user->id));
 794          $this->assertFalse($badge->is_issued($user2->id));
 795          $this->assertFalse($badge->is_issued($user3->id));
 796  
 797          // Verify that after calling review_all_criteria, users with the criteria (user3) award the badge instantly.
 798          $this->assertSame(1, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
 799          $this->assertFalse($badge->is_issued($this->user->id));
 800          $this->assertFalse($badge->is_issued($user2->id));
 801          $this->assertTrue($badge->is_issued($user3->id));
 802          $this->assertDebuggingCalled();
 803  
 804          // Add the user to the cohort1.
 805          cohort_add_member($cohort1->id, $this->user->id);
 806  
 807          // Verify that the badge was not awarded yet (ALL cohorts are needed).
 808          $this->assertFalse($badge->is_issued($this->user->id));
 809          $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
 810  
 811          // Add the user to the cohort3.
 812          cohort_add_member($cohort3->id, $this->user->id);
 813  
 814          // Verify that the badge was not awarded yet (ALL cohorts are needed).
 815          $this->assertFalse($badge->is_issued($this->user->id));
 816          $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
 817  
 818          // Add user to cohort2.
 819          cohort_add_member($cohort2->id, $this->user->id);
 820          $this->assertDebuggingCalled();
 821  
 822          // Verify that the badge was awarded (ALL cohorts).
 823          $this->assertTrue($badge->is_issued($this->user->id));
 824          // As the badge has been awarded to user because core_badges_observer been called when the member has been added to the
 825          // cohort, there are no other users that can award this badge.
 826          $this->assertSame(0, $badge->review_all_criteria()); // Verify award_criteria_cohort->get_completed_criteria_sql().
 827      }
 828  
 829      /**
 830       * Test badges assertion generated when a badge is issued.
 831       */
 832      public function test_badges_assertion() {
 833          $this->preventResetByRollback(); // Messaging is not compatible with transactions.
 834          $badge = new badge($this->coursebadge);
 835          $this->assertFalse($badge->is_issued($this->user->id));
 836  
 837          $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
 838          $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
 839          $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
 840          $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address'));
 841  
 842          $this->user->address = 'Test address';
 843          $sink = $this->redirectEmails();
 844          user_update_user($this->user, false);
 845          $this->assertCount(1, $sink->get_messages());
 846          $sink->close();
 847          // Check if badge is awarded.
 848          $this->assertDebuggingCalled('Error baking badge image!');
 849          $awards = $badge->get_awards();
 850          $this->assertCount(1, $awards);
 851  
 852          // Get assertion.
 853          $award = reset($awards);
 854          $assertion = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V1);
 855          $testassertion = $this->assertion;
 856  
 857          // Make sure JSON strings have the same structure.
 858          $this->assertStringMatchesFormat($testassertion->badge, json_encode($assertion->get_badge_assertion()));
 859          $this->assertStringMatchesFormat($testassertion->class, json_encode($assertion->get_badge_class()));
 860          $this->assertStringMatchesFormat($testassertion->issuer, json_encode($assertion->get_issuer()));
 861  
 862          // Test Openbadge specification version 2.
 863          // Get assertion version 2.
 864          $award = reset($awards);
 865          $assertion2 = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V2);
 866          $testassertion2 = $this->assertion2;
 867  
 868          // Make sure JSON strings have the same structure.
 869          $this->assertStringMatchesFormat($testassertion2->badge, json_encode($assertion2->get_badge_assertion()));
 870          $this->assertStringMatchesFormat($testassertion2->class, json_encode($assertion2->get_badge_class()));
 871          $this->assertStringMatchesFormat($testassertion2->issuer, json_encode($assertion2->get_issuer()));
 872  
 873          // Test Openbadge specification version 2.1. It has the same format as OBv2.0.
 874          // Get assertion version 2.1.
 875          $award = reset($awards);
 876          $assertion2 = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V2P1);
 877  
 878          // Make sure JSON strings have the same structure.
 879          $this->assertStringMatchesFormat($testassertion2->badge, json_encode($assertion2->get_badge_assertion()));
 880          $this->assertStringMatchesFormat($testassertion2->class, json_encode($assertion2->get_badge_class()));
 881          $this->assertStringMatchesFormat($testassertion2->issuer, json_encode($assertion2->get_issuer()));
 882      }
 883  
 884      /**
 885       * Tests the core_badges_myprofile_navigation() function.
 886       */
 887      public function test_core_badges_myprofile_navigation() {
 888          // Set up the test.
 889          $tree = new \core_user\output\myprofile\tree();
 890          $this->setAdminUser();
 891          $badge = new badge($this->badgeid);
 892          $badge->issue($this->user->id, true);
 893          $iscurrentuser = true;
 894          $course = null;
 895  
 896          // Enable badges.
 897          set_config('enablebadges', true);
 898  
 899          // Check the node tree is correct.
 900          core_badges_myprofile_navigation($tree, $this->user, $iscurrentuser, $course);
 901          $reflector = new ReflectionObject($tree);
 902          $nodes = $reflector->getProperty('nodes');
 903          $nodes->setAccessible(true);
 904          $this->assertArrayHasKey('localbadges', $nodes->getValue($tree));
 905      }
 906  
 907      /**
 908       * Tests the core_badges_myprofile_navigation() function with badges disabled..
 909       */
 910      public function test_core_badges_myprofile_navigation_badges_disabled() {
 911          // Set up the test.
 912          $tree = new \core_user\output\myprofile\tree();
 913          $this->setAdminUser();
 914          $badge = new badge($this->badgeid);
 915          $badge->issue($this->user->id, true);
 916          $iscurrentuser = false;
 917          $course = null;
 918  
 919          // Disable badges.
 920          set_config('enablebadges', false);
 921  
 922          // Check the node tree is correct.
 923          core_badges_myprofile_navigation($tree, $this->user, $iscurrentuser, $course);
 924          $reflector = new ReflectionObject($tree);
 925          $nodes = $reflector->getProperty('nodes');
 926          $nodes->setAccessible(true);
 927          $this->assertArrayNotHasKey('localbadges', $nodes->getValue($tree));
 928      }
 929  
 930      /**
 931       * Tests the core_badges_myprofile_navigation() function with a course badge.
 932       */
 933      public function test_core_badges_myprofile_navigation_with_course_badge() {
 934          // Set up the test.
 935          $tree = new \core_user\output\myprofile\tree();
 936          $this->setAdminUser();
 937          $badge = new badge($this->coursebadge);
 938          $badge->issue($this->user->id, true);
 939          $iscurrentuser = false;
 940  
 941          // Check the node tree is correct.
 942          core_badges_myprofile_navigation($tree, $this->user, $iscurrentuser, $this->course);
 943          $reflector = new ReflectionObject($tree);
 944          $nodes = $reflector->getProperty('nodes');
 945          $nodes->setAccessible(true);
 946          $this->assertArrayHasKey('localbadges', $nodes->getValue($tree));
 947      }
 948  
 949      /**
 950       * Test insert and update endorsement with a site badge.
 951       */
 952      public function test_badge_endorsement() {
 953          $badge = new badge($this->badgeid);
 954  
 955          // Insert Endorsement.
 956          $endorsement = new stdClass();
 957          $endorsement->badgeid = $this->badgeid;
 958          $endorsement->issuername = "Issuer 123";
 959          $endorsement->issueremail = "issuer123@email.com";
 960          $endorsement->issuerurl = "https://example.org/issuer-123";
 961          $endorsement->dateissued = 1524567747;
 962          $endorsement->claimid = "https://example.org/robotics-badge.json";
 963          $endorsement->claimcomment = "Test endorser comment";
 964  
 965          $badge->save_endorsement($endorsement);
 966          $endorsement1 = $badge->get_endorsement();
 967          $this->assertEquals($endorsement->badgeid, $endorsement1->badgeid);
 968          $this->assertEquals($endorsement->issuername, $endorsement1->issuername);
 969          $this->assertEquals($endorsement->issueremail, $endorsement1->issueremail);
 970          $this->assertEquals($endorsement->issuerurl, $endorsement1->issuerurl);
 971          $this->assertEquals($endorsement->dateissued, $endorsement1->dateissued);
 972          $this->assertEquals($endorsement->claimid, $endorsement1->claimid);
 973          $this->assertEquals($endorsement->claimcomment, $endorsement1->claimcomment);
 974  
 975          // Update Endorsement.
 976          $endorsement1->issuername = "Issuer update";
 977          $badge->save_endorsement($endorsement1);
 978          $endorsement2 = $badge->get_endorsement();
 979          $this->assertEquals($endorsement1->id, $endorsement2->id);
 980          $this->assertEquals($endorsement1->issuername, $endorsement2->issuername);
 981      }
 982  
 983      /**
 984       * Test insert and delete related badge with a site badge.
 985       */
 986      public function test_badge_related() {
 987          $badge = new badge($this->badgeid);
 988          $newid1 = $badge->make_clone();
 989          $newid2 = $badge->make_clone();
 990          $newid3 = $badge->make_clone();
 991  
 992          // Insert an related badge.
 993          $badge->add_related_badges([$newid1, $newid2, $newid3]);
 994          $this->assertCount(3, $badge->get_related_badges());
 995  
 996          // Only get related is active.
 997          $clonedbage1 = new badge($newid1);
 998          $clonedbage1->status = BADGE_STATUS_ACTIVE;
 999          $clonedbage1->save();
1000          $this->assertCount(1, $badge->get_related_badges(true));
1001  
1002          // Delete an related badge.
1003          $badge->delete_related_badge($newid2);
1004          $this->assertCount(2, $badge->get_related_badges());
1005      }
1006  
1007      /**
1008       * Test insert, update, delete alignment with a site badge.
1009       */
1010      public function test_alignments() {
1011          $badge = new badge($this->badgeid);
1012  
1013          // Insert a alignment.
1014          $alignment1 = new stdClass();
1015          $alignment1->badgeid = $this->badgeid;
1016          $alignment1->targetname = 'CCSS.ELA-Literacy.RST.11-12.3';
1017          $alignment1->targeturl = 'http://www.corestandards.org/ELA-Literacy/RST/11-12/3';
1018          $alignment1->targetdescription = 'Test target description';
1019          $alignment1->targetframework = 'CCSS.RST.11-12.3';
1020          $alignment1->targetcode = 'CCSS.RST.11-12.3';
1021          $alignment2 = clone $alignment1;
1022          $newid1 = $badge->save_alignment($alignment1);
1023          $newid2 = $badge->save_alignment($alignment2);
1024          $alignments1 = $badge->get_alignments();
1025          $this->assertCount(2, $alignments1);
1026  
1027          $this->assertEquals($alignment1->badgeid, $alignments1[$newid1]->badgeid);
1028          $this->assertEquals($alignment1->targetname, $alignments1[$newid1]->targetname);
1029          $this->assertEquals($alignment1->targeturl, $alignments1[$newid1]->targeturl);
1030          $this->assertEquals($alignment1->targetdescription, $alignments1[$newid1]->targetdescription);
1031          $this->assertEquals($alignment1->targetframework, $alignments1[$newid1]->targetframework);
1032          $this->assertEquals($alignment1->targetcode, $alignments1[$newid1]->targetcode);
1033  
1034          // Update aligment.
1035          $alignments1[$newid1]->targetname = 'CCSS.ELA-Literacy.RST.11-12.3 update';
1036          $badge->save_alignment($alignments1[$newid1], $alignments1[$newid1]->id);
1037          $alignments2 = $badge->get_alignments();
1038          $this->assertEquals($alignments1[$newid1]->id, $alignments2[$newid1]->id);
1039          $this->assertEquals($alignments1[$newid1]->targetname, $alignments2[$newid1]->targetname);
1040  
1041          // Delete alignment.
1042          $badge->delete_alignment($alignments1[$newid2]->id);
1043          $this->assertCount(1, $badge->get_alignments());
1044      }
1045  
1046      /**
1047       * Test badges_delete_site_backpack().
1048       *
1049       */
1050      public function test_badges_delete_site_backpack(): void {
1051          global $DB;
1052  
1053          $this->setAdminUser();
1054  
1055          // Create one backpack.
1056          $total = $DB->count_records('badge_external_backpack');
1057          $this->assertEquals(1, $total);
1058  
1059          $data = new \stdClass();
1060          $data->apiversion = OPEN_BADGES_V2P1;
1061          $data->backpackapiurl = 'https://dc.imsglobal.org/obchost/ims/ob/v2p1';
1062          $data->backpackweburl = 'https://dc.imsglobal.org';
1063          badges_create_site_backpack($data);
1064          $backpack = $DB->get_record('badge_external_backpack', ['backpackweburl' => $data->backpackweburl]);
1065          $user1 = $this->getDataGenerator()->create_user();
1066          $user2 = $this->getDataGenerator()->create_user();
1067          // User1 is connected to the backpack to be removed and has 2 collections.
1068          $backpackuser1 = helper::create_fake_backpack(['userid' => $user1->id, 'externalbackpackid' => $backpack->id]);
1069          helper::create_fake_backpack_collection(['backpackid' => $backpackuser1->id]);
1070          helper::create_fake_backpack_collection(['backpackid' => $backpackuser1->id]);
1071          // User2 is connected to a different backpack and has 1 collection.
1072          $backpackuser2 = helper::create_fake_backpack(['userid' => $user2->id]);
1073          helper::create_fake_backpack_collection(['backpackid' => $backpackuser2->id]);
1074  
1075          $total = $DB->count_records('badge_external_backpack');
1076          $this->assertEquals(2, $total);
1077          $total = $DB->count_records('badge_backpack');
1078          $this->assertEquals(2, $total);
1079          $total = $DB->count_records('badge_external');
1080          $this->assertEquals(3, $total);
1081  
1082          // Remove the backpack created previously.
1083          $result = badges_delete_site_backpack($backpack->id);
1084          $this->assertTrue($result);
1085  
1086          $total = $DB->count_records('badge_external_backpack');
1087          $this->assertEquals(1, $total);
1088  
1089          $total = $DB->count_records('badge_backpack');
1090          $this->assertEquals(1, $total);
1091  
1092          $total = $DB->count_records('badge_external');
1093          $this->assertEquals(1, $total);
1094  
1095          // Try to remove an non-existent backpack.
1096          $result = badges_delete_site_backpack($backpack->id);
1097          $this->assertFalse($result);
1098      }
1099  
1100      /**
1101       * Test to validate badges_save_backpack_credentials.
1102       *
1103       * @dataProvider save_backpack_credentials_provider
1104       * @param  bool $addbackpack True if backpack data has to be created; false otherwise (empty data will be used then).
1105       * @param  string|null  $mail  Backpack mail address.
1106       * @param  string|null  $password  Backpack password.
1107       */
1108      public function test_save_backpack_credentials(bool $addbackpack = true, ?string $mail = null, ?string $password = null) {
1109          global $DB;
1110  
1111          $this->resetAfterTest();
1112          $this->setAdminUser();
1113  
1114          $data = [];
1115          if ($addbackpack) {
1116              $data = new \stdClass();
1117              $data->apiversion = OPEN_BADGES_V2P1;
1118              $data->backpackapiurl = 'https://dc.imsglobal.org/obchost/ims/ob/v2p1';
1119              $data->backpackweburl = 'https://dc.imsglobal.org';
1120              badges_create_site_backpack($data);
1121              $backpack = $DB->get_record('badge_external_backpack', ['backpackweburl' => $data->backpackweburl]);
1122              $user = $this->getDataGenerator()->create_user();
1123  
1124              $data = [
1125                  'externalbackpackid' => $backpack->id,
1126                  'userid' => $user->id,
1127              ];
1128  
1129              if (!empty($mail)) {
1130                  $data['backpackemail'] = $mail;
1131              }
1132              if (!empty($password)) {
1133                  $data['password'] = $password;
1134              }
1135          }
1136  
1137          $return = badges_save_backpack_credentials((object) $data);
1138          if (array_key_exists('userid', $data)) {
1139              $record = $DB->get_record('badge_backpack', ['userid' => $user->id]);
1140          } else {
1141              $record = $DB->get_records('badge_backpack');
1142          }
1143  
1144          if (!empty($mail) && !empty($password)) {
1145              // The backpack credentials are created if the given information is right.
1146              $this->assertNotEmpty($record);
1147              $this->assertEquals($data['externalbackpackid'], $return);
1148          } else if ($addbackpack) {
1149              // If no email and password are given, no backpack is created/modified.
1150              $this->assertEmpty($record);
1151              $this->assertEquals($data['externalbackpackid'], $return);
1152          } else {
1153              // There weren't fields to add to the backpack so no DB change is expected.
1154              $this->assertEmpty($record);
1155              $this->assertEquals(0, $return);
1156          }
1157  
1158          // Confirm the existing backpack credential can be updated (if it has been created).
1159          if (!empty($record)) {
1160              $data['backpackemail'] = 'modified_' . $mail;
1161              $data['id'] = $record->id;
1162              $return = badges_save_backpack_credentials((object) $data);
1163              $record = $DB->get_record('badge_backpack', ['userid' => $user->id]);
1164  
1165              $this->assertNotEmpty($record);
1166              $this->assertEquals($data['backpackemail'], $record->email);
1167              $this->assertEquals($data['externalbackpackid'], $return);
1168          }
1169      }
1170  
1171      /**
1172       * Data provider for test_create_backpack_credentials().
1173       *
1174       * @return array
1175       */
1176      public function save_backpack_credentials_provider(): array {
1177          return [
1178              'Empty fields' => [
1179                  false,
1180              ],
1181              'No backpack mail or password are defined' => [
1182                  true,
1183              ],
1184              'Both backpack mail and password are defined' => [
1185                  true, 'test@test.com', '1234',
1186              ],
1187              'Only backpack mail is defined (no password is given)' => [
1188                  true, 'test@test.com', null,
1189              ],
1190              'Only backpack password is defined (no mail is given)' => [
1191                  true, null, '1234'
1192              ],
1193          ];
1194      }
1195  
1196      /**
1197       * Test badges_save_external_backpack.
1198       *
1199       * @dataProvider badges_save_external_backpack_provider
1200       * @param  array $data  Backpack data to save.
1201       * @param  bool $adduser True if a real user has to be used for creating the backpack; false otherwise.
1202       * @param  bool $duplicates True if duplicates has to be tested too; false otherwise.
1203       */
1204      public function test_badges_save_external_backpack(array $data, bool $adduser, bool $duplicates) {
1205          global $DB;
1206  
1207          $this->resetAfterTest();
1208  
1209          $userid = 0;
1210          if ($adduser) {
1211              $user = $this->getDataGenerator()->create_user();
1212              $userid = $user->id;
1213              $data['userid'] = $user->id;
1214          }
1215  
1216          $result = badges_save_external_backpack((object) $data);
1217          $this->assertNotEquals(0, $result);
1218          $record = $DB->get_record('badge_external_backpack', ['id' => $result]);
1219          $this->assertEquals($record->backpackweburl, $data['backpackweburl']);
1220          $this->assertEquals($record->backpackapiurl, $data['backpackapiurl']);
1221  
1222          $record = $DB->get_record('badge_backpack', ['externalbackpackid' => $result]);
1223          if (!array_key_exists('backpackemail', $data) && !array_key_exists('password', $data)) {
1224              $this->assertEmpty($record);
1225              $total = $DB->count_records('badge_backpack');
1226              $this->assertEquals(0, $total);
1227          } else {
1228              $this->assertNotEmpty($record);
1229              $this->assertEquals($record->userid, $userid);
1230          }
1231  
1232          if ($duplicates) {
1233              // We shouldn't be able to insert multiple external_backpacks with the same values.
1234              $this->expectException('dml_write_exception');
1235              $result = badges_save_external_backpack((object)$data);
1236          }
1237      }
1238  
1239      /**
1240       * Provider for test_badges_save_external_backpack
1241       *
1242       * @return array
1243       */
1244      public function badges_save_external_backpack_provider() {
1245          $data = [
1246              'apiversion' => 2,
1247              'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1248              'backpackweburl' => 'https://ca.badgr.io',
1249          ];
1250          return [
1251              'Test without user and auth details. Check duplicates too' => [
1252                  'data' => $data,
1253                  'adduser' => false,
1254                  'duplicates' => true,
1255              ],
1256              'Test without user and auth details. No duplicates' => [
1257                  'data' => $data,
1258                  'adduser' => false,
1259                  'duplicates' => false,
1260              ],
1261              'Test with user and without auth details' => [
1262                  'data' => $data,
1263                  'adduser' => true,
1264                  'duplicates' => false,
1265              ],
1266              'Test with user and without auth details. Check duplicates too' => [
1267                  'data' => $data,
1268                  'adduser' => true,
1269                  'duplicates' => true,
1270              ],
1271              'Test with empty backpackemail, password and id' => [
1272                  'data' => array_merge($data, [
1273                      'backpackemail' => '',
1274                      'password' => '',
1275                      'id' => 0,
1276                  ]),
1277                  'adduser' => false,
1278                  'duplicates' => false,
1279              ],
1280              'Test with empty backpackemail, password and id but with user' => [
1281                  'data' => array_merge($data, [
1282                      'backpackemail' => '',
1283                      'password' => '',
1284                      'id' => 0,
1285                  ]),
1286                  'adduser' => true,
1287                  'duplicates' => false,
1288              ],
1289              'Test with auth details but without user' => [
1290                  'data' => array_merge($data, [
1291                      'backpackemail' => 'test@test.com',
1292                      'password' => 'test',
1293                  ]),
1294                  'adduser' => false,
1295                  'duplicates' => false,
1296              ],
1297              'Test with auth details and user' => [
1298                  'data' => array_merge($data, [
1299                      'backpackemail' => 'test@test.com',
1300                      'password' => 'test',
1301                  ]),
1302                  'adduser' => true,
1303                  'duplicates' => false,
1304              ],
1305          ];
1306      }
1307  
1308      /**
1309       * Test backpack creation/update with auth details provided
1310       *
1311       * @param boolean $isadmin
1312       * @param boolean $updatetest
1313       * @dataProvider badges_create_site_backpack_provider
1314       */
1315      public function test_badges_create_site_backpack($isadmin, $updatetest) {
1316          global $DB;
1317          $this->resetAfterTest();
1318  
1319          $data = [
1320              'apiversion' => 2,
1321              'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1322              'backpackweburl' => 'https://ca.badgr.io',
1323          ];
1324  
1325          $data['backpackemail'] = 'test@test.com';
1326          $data['password'] = 'test';
1327          if ($isadmin || $updatetest) {
1328              $this->setAdminUser();
1329              $lastmax = $DB->get_field_sql('SELECT MAX(sortorder) FROM {badge_external_backpack}');
1330              $backpack = badges_create_site_backpack((object) $data);
1331          }
1332  
1333          if ($isadmin) {
1334              if ($updatetest) {
1335                  $record = $DB->get_record('badge_backpack', ['userid' => 0]);
1336                  $data['badgebackpack'] = $record->id;
1337                  $data['backpackapiurl'] = 'https://api.ca.badgr.io/v3';
1338                  badges_update_site_backpack($backpack, (object)$data);
1339              }
1340              $record = $DB->get_record('badge_external_backpack', ['id' => $backpack]);
1341              $this->assertEquals($data['backpackweburl'], $record->backpackweburl);
1342              $this->assertEquals($data['backpackapiurl'], $record->backpackapiurl);
1343              $this->assertEquals($lastmax + 1, $record->sortorder);
1344              $record = $DB->get_record('badge_backpack', ['userid' => 0]);
1345              $this->assertNotEmpty($record);
1346          } else {
1347              $user = $this->getDataGenerator()->create_user();
1348              $this->setUser($user);
1349              $this->expectException('required_capability_exception');
1350              if ($updatetest) {
1351                  $result = badges_update_site_backpack($backpack, (object) $data);
1352              } else {
1353                  $result = badges_create_site_backpack((object)$data);
1354              }
1355          }
1356      }
1357  
1358      /**
1359       * Provider for test_badges_(create/update)_site_backpack
1360       */
1361      public function badges_create_site_backpack_provider() {
1362          return [
1363              "Test as admin user - creation test" => [true, true],
1364              "Test as admin user - update test" => [true, false],
1365              "Test as normal user - creation test" => [false, true],
1366              "Test as normal user - update test" => [false, false],
1367          ];
1368      }
1369  
1370      /**
1371       * Test the badges_open_badges_backpack_api with different backpacks
1372       */
1373      public function test_badges_open_badges_backpack_api() {
1374          $this->resetAfterTest();
1375  
1376          $data = [
1377              'apiversion' => 2,
1378              'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1379              'backpackweburl' => 'https://ca.badgr.io',
1380              'sortorder' => 2,
1381          ];
1382  
1383          // Given a complete set of unique data, a new backpack and auth records should exist in the tables.
1384          $data['backpackemail'] = 'test@test.com';
1385          $data['password'] = 'test';
1386          $backpack1 = badges_save_external_backpack((object) $data);
1387          $data['backpackweburl'] = 'https://eu.badgr.io';
1388          $data['backpackapiurl'] = 'https://api.eu.badgr.io/v2';
1389          $data['apiversion'] = '2.1';
1390          $data['sortorder'] = 3;
1391          $backpack2 = badges_save_external_backpack((object) $data);
1392  
1393          // Move backpack2 to the first position to set it as primary site backpack.
1394          $this->move_backpack_to_first_position($backpack2);
1395  
1396          // The default response should check the default site backpack api version.
1397          $this->assertEquals(2.1, badges_open_badges_backpack_api());
1398          // Check the api version for the other backpack created.
1399          $this->assertEquals(2, badges_open_badges_backpack_api($backpack1));
1400          $this->assertEquals(2.1, badges_open_badges_backpack_api($backpack2));
1401      }
1402  
1403      /**
1404       * Test the badges_get_site_backpack function
1405       */
1406      public function test_badges_get_site_backpack() {
1407          $this->resetAfterTest();
1408          $user = $this->getDataGenerator()->create_user();
1409          $data = [
1410              'apiversion' => '2',
1411              'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1412              'backpackweburl' => 'https://ca.badgr.io',
1413          ];
1414          $backpack1 = badges_save_external_backpack((object) $data);
1415          $data2 = array_merge($data, [
1416              'backpackapiurl' => 'https://api.eu.badgr.io/v2',
1417              'backpackweburl' => 'https://eu.badgr.io',
1418              'backpackemail' => 'test@test.com',
1419              'password' => 'test',
1420          ]);
1421          $backpack2 = badges_save_external_backpack((object) $data2);
1422          $data3 = array_merge($data2, [
1423              'userid' => $user->id,
1424              'externalbackpackid' => $backpack2,
1425              'backpackemail' => 'test2@test.com'
1426          ]);
1427          // In the following case, the id returned below equals backpack2. So we aren't storing it.
1428          badges_save_backpack_credentials((object) $data3);
1429          unset($data3['userid']);
1430  
1431          // Get a site back based on the id returned from creation and no user id provided.
1432          $this->assertEquals($data, array_intersect($data, (array) badges_get_site_backpack($backpack1)));
1433          $this->assertEquals($data2, array_intersect($data2, (array) badges_get_site_backpack($backpack2)));
1434          $this->assertEquals($data2, array_intersect($data2, (array) badges_get_site_backpack($backpack2, 0)));
1435          $this->assertEquals($data3, array_intersect($data3, (array) badges_get_site_backpack($backpack2, $user->id)));
1436  
1437          // Non-existent user backpack should return only configuration details and not auth details.
1438          $userbackpack = badges_get_site_backpack($backpack1, $user->id);
1439          $this->assertNull($userbackpack->badgebackpack);
1440          $this->assertNull($userbackpack->password);
1441          $this->assertNull($userbackpack->backpackemail);
1442      }
1443  
1444      /**
1445       * Test the badges_get_user_backpack function
1446       */
1447      public function test_badges_get_user_backpack() {
1448          $this->resetAfterTest();
1449          $user = $this->getDataGenerator()->create_user();
1450          $data = [
1451              'apiversion' => '2',
1452              'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1453              'backpackweburl' => 'https://ca.badgr.io',
1454          ];
1455          $backpack1 = badges_save_external_backpack((object) $data);
1456          $data2 = array_merge($data, [
1457              'backpackapiurl' => 'https://api.eu.badgr.io/v2',
1458              'backpackweburl' => 'https://eu.badgr.io',
1459              'backpackemail' => 'test@test.com',
1460              'password' => 'test',
1461          ]);
1462          $backpack2 = badges_save_external_backpack((object) $data2);
1463          $data3 = array_merge($data2, [
1464              'userid' => $user->id,
1465              'externalbackpackid' => $backpack2,
1466              'backpackemail' => 'test2@test.com'
1467          ]);
1468          // In the following case, the id returned below equals backpack2. So we aren't storing it.
1469          badges_save_backpack_credentials((object) $data3);
1470          unset($data3['userid']);
1471  
1472          // Currently logged in as admin.
1473          $this->assertEquals($data2, array_intersect($data2, (array) badges_get_user_backpack()));
1474          $this->assertEquals($data2, array_intersect($data2, (array) badges_get_user_backpack(0)));
1475          $this->assertEquals($data3, array_intersect($data3, (array) badges_get_user_backpack($user->id)));
1476  
1477          // Non-existent user backpack should return nothing.
1478          $this->assertFalse(badges_get_user_backpack($backpack1, $user->id));
1479  
1480          // Login as user.
1481          $this->setUser($user);
1482          $this->assertEquals($data3, array_intersect($data3, (array) badges_get_user_backpack()));
1483      }
1484  
1485      /**
1486       * Test the badges_get_site_primary_backpack function
1487       *
1488       * @param boolean $withauth Testing with authentication or not.
1489       * @dataProvider badges_get_site_primary_backpack_provider
1490       */
1491      public function test_badges_get_site_primary_backpack($withauth) {
1492          $data = [
1493              'apiversion' => '2',
1494              'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1495              'backpackweburl' => 'https://ca.badgr.io',
1496              'sortorder' => '2',
1497          ];
1498          if ($withauth) {
1499              $data = array_merge($data, [
1500                  'backpackemail' => 'test@test.com',
1501                  'password' => 'test',
1502              ]);
1503          }
1504          $backpack = badges_save_external_backpack((object) $data);
1505  
1506          // Check the backpack created is not the primary one.
1507          $sitebackpack = badges_get_site_primary_backpack();
1508          $this->assertNotEquals($backpack, $sitebackpack->id);
1509  
1510          // Move backpack to the first position to set it as primary site backpack.
1511          $this->move_backpack_to_first_position($backpack);
1512  
1513          $sitebackpack = badges_get_site_primary_backpack();
1514          $this->assertEquals($backpack, $sitebackpack->id);
1515  
1516          if ($withauth) {
1517              $this->assertEquals($data, array_intersect($data, (array) $sitebackpack));
1518              $this->assertEquals($data['password'], $sitebackpack->password);
1519              $this->assertEquals($data['backpackemail'], $sitebackpack->backpackemail);
1520          } else {
1521              $this->assertNull($sitebackpack->badgebackpack);
1522              $this->assertNull($sitebackpack->password);
1523              $this->assertNull($sitebackpack->backpackemail);
1524          }
1525      }
1526  
1527      /**
1528       * Test the test_badges_get_site_primary_backpack function.
1529       *
1530       * @return array
1531       */
1532      public function badges_get_site_primary_backpack_provider() {
1533          return [
1534              "Test with auth details" => [true],
1535              "Test without auth details" => [false],
1536          ];
1537      }
1538  
1539      /**
1540       * Test badges_change_sortorder_backpacks().
1541       *
1542       * @dataProvider badges_change_sortorder_backpacks_provider
1543       * @covers ::badges_change_sortorder_backpacks
1544       *
1545       * @param int $backpacktomove Backpack index to move (from 0 to 5).
1546       * @param int $direction Direction to move the backpack.
1547       * @param int|null $expectedsortorder Expected sortorder or null if an exception is expected.
1548       */
1549      public function test_badges_change_sortorder_backpacks(int $backpacktomove, int $direction, ?int $expectedsortorder): void {
1550          global $DB;
1551  
1552          $this->resetAfterTest();
1553          $this->setAdminUser();
1554  
1555          // Create 5 more backpacks.
1556          for ($i = 0; $i < 5; $i++) {
1557              $data = new \stdClass();
1558              $data->apiversion = OPEN_BADGES_V2P1;
1559              $data->backpackapiurl = "https://myurl$i.cat/ob/v2p1";
1560              $data->backpackweburl = "https://myurl$i.cat";
1561              badges_create_site_backpack($data);
1562          }
1563  
1564          // Check there are 6 backpacks (1 pre-existing + 5 news).
1565          $total = $DB->count_records('badge_external_backpack');
1566          $this->assertEquals(6, $total);
1567          $backpacks = array_values(badges_get_site_backpacks());
1568  
1569          if (is_null($expectedsortorder)) {
1570              $this->expectException('moodle_exception');
1571          }
1572  
1573          // Move the backpack.
1574          badges_change_sortorder_backpacks($backpacks[$backpacktomove]->id, $direction);
1575  
1576          if (!is_null($expectedsortorder)) {
1577              $backpack = badges_get_site_backpack($backpacks[$backpacktomove]->id);
1578              $this->assertEquals($expectedsortorder, $backpack->sortorder);
1579          }
1580      }
1581  
1582      /**
1583       * Provider for test_badges_change_sortorder_backpacks.
1584       *
1585       * @return array
1586       */
1587      public function badges_change_sortorder_backpacks_provider(): array {
1588          return [
1589              "Test up" => [
1590                  'backpacktomove' => 1,
1591                  'direction' => BACKPACK_MOVE_UP,
1592                  'expectedsortorder' => 1,
1593              ],
1594              "Test down" => [
1595                  'backpacktomove' => 1,
1596                  'direction' => BACKPACK_MOVE_DOWN,
1597                  'expectedsortorder' => 3,
1598              ],
1599              "Test up the very first element" => [
1600                  'backpacktomove' => 0,
1601                  'direction' => BACKPACK_MOVE_UP,
1602                  'expectedsortorder' => 1,
1603              ],
1604              "Test down the very last element" => [
1605                  'backpacktomove' => 5,
1606                  'direction' => BACKPACK_MOVE_DOWN,
1607                  'expectedsortorder' => 6,
1608              ],
1609              "Test with an invalid direction value" => [
1610                  'backpacktomove' => 1,
1611                  'direction' => 10,
1612                  'expectedsortorder' => null,
1613              ],
1614          ];
1615      }
1616  
1617      /**
1618       * Test the Badgr URL generator function
1619       *
1620       * @param mixed $type Type corresponding to the badge entites
1621       * @param string $expected Expected string result
1622       * @dataProvider badgr_open_url_generator
1623       */
1624      public function test_badges_generate_badgr_open_url($type, $expected) {
1625          $data = [
1626              'apiversion' => '2',
1627              'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1628              'backpackweburl' => 'https://ca.badgr.io',
1629              'backpackemail' => 'test@test.com',
1630              'password' => 'test',
1631          ];
1632          $backpack2 = badges_save_external_backpack((object) $data);
1633          $backpack = badges_get_site_backpack($backpack2);
1634          $this->assertEquals($expected, badges_generate_badgr_open_url($backpack, $type, 123455));
1635      }
1636  
1637      /**
1638       * Data provider for test_badges_generate_badgr_open_url
1639       * @return array
1640       */
1641      public function badgr_open_url_generator() {
1642          return [
1643              'Badgr Assertion URL test' => [
1644                  OPEN_BADGES_V2_TYPE_ASSERTION, "https://api.ca.badgr.io/public/assertions/123455"
1645              ],
1646              'Badgr Issuer URL test' => [
1647                  OPEN_BADGES_V2_TYPE_ISSUER, "https://api.ca.badgr.io/public/issuers/123455"
1648              ],
1649              'Badgr Badge URL test' => [
1650                  OPEN_BADGES_V2_TYPE_BADGE, "https://api.ca.badgr.io/public/badges/123455"
1651              ]
1652          ];
1653      }
1654  
1655      /**
1656       * Test badges_external_get_mapping function
1657       *
1658       * @param int $internalid The internal id of the mapping
1659       * @param int $externalid The external / remote ref to the mapping
1660       * @param mixed $expected The expected result from the function
1661       * @param string|null $field The field we are passing to the function. Null if we don't want to pass anything.ss
1662       *
1663       * @dataProvider badges_external_get_mapping_provider
1664       */
1665      public function test_badges_external_get_mapping($internalid, $externalid, $expected, $field = null) {
1666          $data = [
1667              'apiversion' => '2',
1668              'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1669              'backpackweburl' => 'https://ca.badgr.io',
1670              'backpackemail' => 'test@test.com',
1671              'password' => 'test',
1672          ];
1673          $backpack2 = badges_save_external_backpack((object) $data);
1674          badges_external_create_mapping($backpack2, OPEN_BADGES_V2_TYPE_BADGE, $internalid, $externalid);
1675          $expected = $expected == "id" ? $backpack2 : $expected;
1676          if ($field) {
1677              $this->assertEquals($expected, badges_external_get_mapping($backpack2, OPEN_BADGES_V2_TYPE_BADGE, $internalid, $field));
1678          } else {
1679              $this->assertEquals($expected, badges_external_get_mapping($backpack2, OPEN_BADGES_V2_TYPE_BADGE, $internalid));
1680          }
1681      }
1682  
1683      /**
1684       * Data provider for badges_external_get_mapping_provider
1685       *
1686       * @return array
1687       */
1688      public function badges_external_get_mapping_provider() {
1689          return [
1690              "Get the site backpack value" => [
1691                  1234, 4321, 'id', 'sitebackpackid'
1692              ],
1693              "Get the type of the mapping" => [
1694                  1234, 4321, OPEN_BADGES_V2_TYPE_BADGE, 'type'
1695              ],
1696              "Get the externalid of the mapping" => [
1697                  1234, 4321, 4321, 'externalid'
1698              ],
1699              "Get the externalid of the mapping without providing a param" => [
1700                  1234, 4321, 4321, null
1701              ],
1702              "Get the internalid of the mapping" => [
1703                  1234, 4321, 1234, 'internalid'
1704              ]
1705          ];
1706      }
1707  
1708      /**
1709       * Move the backpack to the first position, to set it as primary site backpack.
1710       *
1711       * @param int $backpackid The backpack identifier.
1712       */
1713      private function move_backpack_to_first_position(int $backpackid): void {
1714          $backpack = badges_get_site_backpack($backpackid);
1715          while ($backpack->sortorder > 1) {
1716              badges_change_sortorder_backpacks($backpackid, BACKPACK_MOVE_UP);
1717              $backpack = badges_get_site_backpack($backpackid);
1718          }
1719      }
1720  }