Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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