MDL-70059 core_badges: avoid duplicate key error
[moodle.git] / badges / tests / badgeslib_test.php
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/>.
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  */
27 defined('MOODLE_INTERNAL') || die();
29 global $CFG;
30 require_once($CFG->libdir . '/badgeslib.php');
31 require_once($CFG->dirroot . '/badges/lib.php');
33 use core_badges\helper;
35 class core_badges_badgeslib_testcase extends advanced_testcase {
36     protected $badgeid;
37     protected $course;
38     protected $user;
39     protected $module;
40     protected $coursebadge;
41     protected $assertion;
43     /** @var $assertion2 to define json format for Open badge version 2 */
44     protected $assertion2;
46     protected function setUp(): void {
47         global $DB, $CFG;
48         $this->resetAfterTest(true);
49         $CFG->enablecompletion = true;
50         $user = $this->getDataGenerator()->create_user();
51         $fordb = new stdClass();
52         $fordb->id = null;
53         $fordb->name = "Test badge with 'apostrophe' and other friends (<>&@#)";
54         $fordb->description = "Testing badges";
55         $fordb->timecreated = time();
56         $fordb->timemodified = time();
57         $fordb->usercreated = $user->id;
58         $fordb->usermodified = $user->id;
59         $fordb->issuername = "Test issuer";
60         $fordb->issuerurl = "http://issuer-url.domain.co.nz";
61         $fordb->issuercontact = "issuer@example.com";
62         $fordb->expiredate = null;
63         $fordb->expireperiod = null;
64         $fordb->type = BADGE_TYPE_SITE;
65         $fordb->version = 1;
66         $fordb->language = 'en';
67         $fordb->courseid = null;
68         $fordb->messagesubject = "Test message subject";
69         $fordb->message = "Test message body";
70         $fordb->attachment = 1;
71         $fordb->notification = 0;
72         $fordb->imageauthorname = "Image Author 1";
73         $fordb->imageauthoremail = "author@example.com";
74         $fordb->imageauthorurl = "http://author-url.example.com";
75         $fordb->imagecaption = "Test caption image";
76         $fordb->status = BADGE_STATUS_INACTIVE;
78         $this->badgeid = $DB->insert_record('badge', $fordb, true);
80         // Set the default Issuer (because OBv2 needs them).
81         set_config('badges_defaultissuername', $fordb->issuername);
82         set_config('badges_defaultissuercontact', $fordb->issuercontact);
84         // Create a course with activity and auto completion tracking.
85         $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
86         $this->user = $this->getDataGenerator()->create_user();
87         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
88         $this->assertNotEmpty($studentrole);
90         // Get manual enrolment plugin and enrol user.
91         require_once($CFG->dirroot.'/enrol/manual/locallib.php');
92         $manplugin = enrol_get_plugin('manual');
93         $maninstance = $DB->get_record('enrol', array('courseid' => $this->course->id, 'enrol' => 'manual'), '*', MUST_EXIST);
94         $manplugin->enrol_user($maninstance, $this->user->id, $studentrole->id);
95         $this->assertEquals(1, $DB->count_records('user_enrolments'));
96         $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
97         $this->module = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
99         // Build badge and criteria.
100         $fordb->type = BADGE_TYPE_COURSE;
101         $fordb->courseid = $this->course->id;
102         $fordb->status = BADGE_STATUS_ACTIVE;
103         $this->coursebadge = $DB->insert_record('badge', $fordb, true);
105         // Insert Endorsement.
106         $endorsement = new stdClass();
107         $endorsement->badgeid = $this->coursebadge;
108         $endorsement->issuername = "Issuer 123";
109         $endorsement->issueremail = "issuer123@email.com";
110         $endorsement->issuerurl = "https://example.org/issuer-123";
111         $endorsement->dateissued = 1524567747;
112         $endorsement->claimid = "https://example.org/robotics-badge.json";
113         $endorsement->claimcomment = "Test endorser comment";
114         $DB->insert_record('badge_endorsement', $endorsement, true);
116         // Insert related badges.
117         $badge = new badge($this->coursebadge);
118         $clonedid = $badge->make_clone();
119         $badgeclone = new badge($clonedid);
120         $badgeclone->status = BADGE_STATUS_ACTIVE;
121         $badgeclone->save();
123         $relatebadge = new stdClass();
124         $relatebadge->badgeid = $this->coursebadge;
125         $relatebadge->relatedbadgeid = $clonedid;
126         $relatebadge->relatedid = $DB->insert_record('badge_related', $relatebadge, true);
128         // Insert a aligment.
129         $alignment = new stdClass();
130         $alignment->badgeid = $this->coursebadge;
131         $alignment->targetname = 'CCSS.ELA-Literacy.RST.11-12.3';
132         $alignment->targeturl = 'http://www.corestandards.org/ELA-Literacy/RST/11-12/3';
133         $alignment->targetdescription = 'Test target description';
134         $alignment->targetframework = 'CCSS.RST.11-12.3';
135         $alignment->targetcode = 'CCSS.RST.11-12.3';
136         $DB->insert_record('badge_alignment', $alignment, true);
138         $this->assertion = new stdClass();
139         $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"}';
140         $this->assertion->class = '{"name":"%s","description":"%s","image":"%s","criteria":"%s","issuer":"%s"}';
141         $this->assertion->issuer = '{"name":"%s","url":"%s","email":"%s"}';
142         // Format JSON-LD for Openbadge specification version 2.0.
143         $this->assertion2 = new stdClass();
144         $this->assertion2->badge = '{"recipient":{"identity":"%s","type":"email","hashed":true,"salt":"%s"},' .
145             '"badge":{"name":"%s","description":"%s","image":"%s",' .
146             '"criteria":{"id":"%s","narrative":"%s"},"issuer":{"name":"%s","url":"%s","email":"%s",' .
147             '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"},' .
148             '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' .
149             '"@language":"en","related":[{"id":"%s","version":"%s","@language":"%s"}],"endorsement":"%s",' .
150             '"alignments":[{"targetName":"%s","targetUrl":"%s","targetDescription":"%s","targetFramework":"%s",' .
151             '"targetCode":"%s"}]},"verify":{"type":"hosted","url":"%s"},"issuedOn":"%s","evidence":"%s",' .
152             '"@context":"https:\/\/w3id.org\/openbadges\/v2","type":"Assertion","id":"%s"}';
154         $this->assertion2->class = '{"name":"%s","description":"%s","image":"%s",' .
155             '"criteria":{"id":"%s","narrative":"%s"},"issuer":{"name":"%s","url":"%s","email":"%s",' .
156             '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"},' .
157             '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"BadgeClass","version":"%s",' .
158             '"@language":"%s","related":[{"id":"%s","version":"%s","@language":"%s"}],"endorsement":"%s",' .
159             '"alignments":[{"targetName":"%s","targetUrl":"%s","targetDescription":"%s","targetFramework":"%s",' .
160             '"targetCode":"%s"}]}';
161         $this->assertion2->issuer = '{"name":"%s","url":"%s","email":"%s",' .
162             '"@context":"https:\/\/w3id.org\/openbadges\/v2","id":"%s","type":"Issuer"}';
163     }
165     public function test_create_badge() {
166         $badge = new badge($this->badgeid);
168         $this->assertInstanceOf('badge', $badge);
169         $this->assertEquals($this->badgeid, $badge->id);
170     }
172     public function test_clone_badge() {
173         $badge = new badge($this->badgeid);
174         $newid = $badge->make_clone();
175         $clonedbadge = new badge($newid);
177         $this->assertEquals($badge->description, $clonedbadge->description);
178         $this->assertEquals($badge->issuercontact, $clonedbadge->issuercontact);
179         $this->assertEquals($badge->issuername, $clonedbadge->issuername);
180         $this->assertEquals($badge->issuercontact, $clonedbadge->issuercontact);
181         $this->assertEquals($badge->issuerurl, $clonedbadge->issuerurl);
182         $this->assertEquals($badge->expiredate, $clonedbadge->expiredate);
183         $this->assertEquals($badge->expireperiod, $clonedbadge->expireperiod);
184         $this->assertEquals($badge->type, $clonedbadge->type);
185         $this->assertEquals($badge->courseid, $clonedbadge->courseid);
186         $this->assertEquals($badge->message, $clonedbadge->message);
187         $this->assertEquals($badge->messagesubject, $clonedbadge->messagesubject);
188         $this->assertEquals($badge->attachment, $clonedbadge->attachment);
189         $this->assertEquals($badge->notification, $clonedbadge->notification);
190         $this->assertEquals($badge->version, $clonedbadge->version);
191         $this->assertEquals($badge->language, $clonedbadge->language);
192         $this->assertEquals($badge->imagecaption, $clonedbadge->imagecaption);
193         $this->assertEquals($badge->imageauthorname, $clonedbadge->imageauthorname);
194         $this->assertEquals($badge->imageauthoremail, $clonedbadge->imageauthoremail);
195         $this->assertEquals($badge->imageauthorurl, $clonedbadge->imageauthorurl);
196     }
198     public function test_badge_status() {
199         $badge = new badge($this->badgeid);
200         $old_status = $badge->status;
201         $badge->set_status(BADGE_STATUS_ACTIVE);
202         $this->assertNotEquals($old_status, $badge->status);
203         $this->assertEquals(BADGE_STATUS_ACTIVE, $badge->status);
204     }
206     public function test_delete_badge() {
207         $badge = new badge($this->badgeid);
208         $badge->delete();
209         // We don't actually delete badges. We archive them.
210         $this->assertEquals(BADGE_STATUS_ARCHIVED, $badge->status);
211     }
213     /**
214      * Really delete the badge.
215      */
216     public function test_delete_badge_for_real() {
217         global $DB;
219         $badge = new badge($this->badgeid);
221         $newid1 = $badge->make_clone();
222         $newid2 = $badge->make_clone();
223         $newid3 = $badge->make_clone();
225         // Insert related badges to badge 1.
226         $badge->add_related_badges([$newid1, $newid2, $newid3]);
228         // Another badge.
229         $badge2 = new badge($newid2);
230         // Make badge 1 related for badge 2.
231         $badge2->add_related_badges([$this->badgeid]);
233         // Confirm that the records about this badge about its relations have been removed as well.
234         $relatedsql = 'badgeid = :badgeid OR relatedbadgeid = :relatedbadgeid';
235         $relatedparams = array(
236             'badgeid' => $this->badgeid,
237             'relatedbadgeid' => $this->badgeid
238         );
239         // Badge 1 has 4 related records. 3 where it's the badgeid, 1 where it's the relatedbadgeid.
240         $this->assertEquals(4, $DB->count_records_select('badge_related', $relatedsql, $relatedparams));
242         // Delete the badge for real.
243         $badge->delete(false);
245         // Confirm that the badge itself has been removed.
246         $this->assertFalse($DB->record_exists('badge', ['id' => $this->badgeid]));
248         // Confirm that the records about this badge about its relations have been removed as well.
249         $this->assertFalse($DB->record_exists_select('badge_related', $relatedsql, $relatedparams));
250     }
252     public function test_create_badge_criteria() {
253         $badge = new badge($this->badgeid);
254         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
255         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL));
257         $this->assertCount(1, $badge->get_criteria());
259         $criteria_profile = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
260         $params = array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address');
261         $criteria_profile->save($params);
263         $this->assertCount(2, $badge->get_criteria());
264     }
266     public function test_add_badge_criteria_description() {
267         $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $this->badgeid));
268         $criteriaoverall->save(array(
269                 'agg' => BADGE_CRITERIA_AGGREGATION_ALL,
270                 'description' => 'Overall description',
271                 'descriptionformat' => FORMAT_HTML
272         ));
274         $criteriaprofile = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $this->badgeid));
275         $params = array(
276                 'agg' => BADGE_CRITERIA_AGGREGATION_ALL,
277                 'field_address' => 'address',
278                 'description' => 'Description',
279                 'descriptionformat' => FORMAT_HTML
280         );
281         $criteriaprofile->save($params);
283         $badge = new badge($this->badgeid);
284         $this->assertEquals('Overall description', $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->description);
285         $this->assertEquals('Description', $badge->criteria[BADGE_CRITERIA_TYPE_PROFILE]->description);
286     }
288     public function test_delete_badge_criteria() {
289         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $this->badgeid));
290         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL));
291         $badge = new badge($this->badgeid);
293         $this->assertInstanceOf('award_criteria_overall', $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]);
295         $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->delete();
296         $this->assertEmpty($badge->get_criteria());
297     }
299     public function test_badge_awards() {
300         global $DB;
301         $this->preventResetByRollback(); // Messaging is not compatible with transactions.
302         $badge = new badge($this->badgeid);
303         $user1 = $this->getDataGenerator()->create_user();
305         $sink = $this->redirectMessages();
307         $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
308         set_user_preference('message_provider_moodle_badgerecipientnotice_loggedoff', 'email', $user1);
310         $badge->issue($user1->id, false);
311         $this->assertDebuggingCalled(); // Expect debugging while baking a badge via phpunit.
312         $this->assertTrue($badge->is_issued($user1->id));
314         $messages = $sink->get_messages();
315         $sink->close();
316         $this->assertCount(1, $messages);
317         $message = array_pop($messages);
318         // Check we have the expected data.
319         $customdata = json_decode($message->customdata);
320         $this->assertObjectHasAttribute('notificationiconurl', $customdata);
321         $this->assertObjectHasAttribute('hash', $customdata);
323         $user2 = $this->getDataGenerator()->create_user();
324         $badge->issue($user2->id, true);
325         $this->assertTrue($badge->is_issued($user2->id));
327         $this->assertCount(2, $badge->get_awards());
328     }
330     /**
331      * Test the {@link badges_get_user_badges()} function in lib/badgeslib.php
332      */
333     public function test_badges_get_user_badges() {
334         global $DB;
336         // Messaging is not compatible with transactions.
337         $this->preventResetByRollback();
339         $badges = array();
340         $user1 = $this->getDataGenerator()->create_user();
341         $user2 = $this->getDataGenerator()->create_user();
343         // Record the current time, we need to be precise about a couple of things.
344         $now = time();
345         // Create 11 badges with which to test.
346         for ($i = 1; $i <= 11; $i++) {
347             // Mock up a badge.
348             $badge = new stdClass();
349             $badge->id = null;
350             $badge->name = "Test badge $i";
351             $badge->description = "Testing badges $i";
352             $badge->timecreated = $now - 12;
353             $badge->timemodified = $now - 12;
354             $badge->usercreated = $user1->id;
355             $badge->usermodified = $user1->id;
356             $badge->issuername = "Test issuer";
357             $badge->issuerurl = "http://issuer-url.domain.co.nz";
358             $badge->issuercontact = "issuer@example.com";
359             $badge->expiredate = null;
360             $badge->expireperiod = null;
361             $badge->type = BADGE_TYPE_SITE;
362             $badge->courseid = null;
363             $badge->messagesubject = "Test message subject for badge $i";
364             $badge->message = "Test message body for badge $i";
365             $badge->attachment = 1;
366             $badge->notification = 0;
367             $badge->status = BADGE_STATUS_INACTIVE;
368             $badge->version = "Version $i";
369             $badge->language = "en";
370             $badge->imagecaption = "Image caption $i";
371             $badge->imageauthorname = "Image author's name $i";
372             $badge->imageauthoremail = "author$i@example.com";
373             $badge->imageauthorname = "Image author's name $i";
375             $badgeid = $DB->insert_record('badge', $badge, true);
376             $badges[$badgeid] = new badge($badgeid);
377             $badges[$badgeid]->issue($user2->id, true);
378             // Check it all actually worked.
379             $this->assertCount(1, $badges[$badgeid]->get_awards());
381             // Hack the database to adjust the time each badge was issued.
382             // The alternative to this is sleep which is a no-no in unit tests.
383             $DB->set_field('badge_issued', 'dateissued', $now - 11 + $i, array('userid' => $user2->id, 'badgeid' => $badgeid));
384         }
386         // Make sure the first user has no badges.
387         $result = badges_get_user_badges($user1->id);
388         $this->assertIsArray($result);
389         $this->assertCount(0, $result);
391         // Check that the second user has the expected 11 badges.
392         $result = badges_get_user_badges($user2->id);
393         $this->assertCount(11, $result);
395         // Test pagination.
396         // Ordering is by time issued desc, so things will come out with the last awarded badge first.
397         $result = badges_get_user_badges($user2->id, 0, 0, 4);
398         $this->assertCount(4, $result);
399         $lastbadgeissued = reset($result);
400         $this->assertSame('Test badge 11', $lastbadgeissued->name);
401         // Page 2. Expecting 4 results again.
402         $result = badges_get_user_badges($user2->id, 0, 1, 4);
403         $this->assertCount(4, $result);
404         $lastbadgeissued = reset($result);
405         $this->assertSame('Test badge 7', $lastbadgeissued->name);
406         // Page 3. Expecting just three results here.
407         $result = badges_get_user_badges($user2->id, 0, 2, 4);
408         $this->assertCount(3, $result);
409         $lastbadgeissued = reset($result);
410         $this->assertSame('Test badge 3', $lastbadgeissued->name);
411         // Page 4.... there is no page 4.
412         $result = badges_get_user_badges($user2->id, 0, 3, 4);
413         $this->assertCount(0, $result);
415         // Test search.
416         $result = badges_get_user_badges($user2->id, 0, 0, 0, 'badge 1');
417         $this->assertCount(3, $result);
418         $lastbadgeissued = reset($result);
419         $this->assertSame('Test badge 11', $lastbadgeissued->name);
420         // The term Totara doesn't appear anywhere in the badges.
421         $result = badges_get_user_badges($user2->id, 0, 0, 0, 'Totara');
422         $this->assertCount(0, $result);
424         // Issue a user with a course badge and verify its returned based on if
425         // coursebadges are enabled or disabled.
426         $sitebadgeid = key($badges);
427         $badges[$sitebadgeid]->issue($this->user->id, true);
429         $badge = new stdClass();
430         $badge->id = null;
431         $badge->name = "Test course badge";
432         $badge->description = "Testing course badge";
433         $badge->timecreated = $now;
434         $badge->timemodified = $now;
435         $badge->usercreated = $user1->id;
436         $badge->usermodified = $user1->id;
437         $badge->issuername = "Test issuer";
438         $badge->issuerurl = "http://issuer-url.domain.co.nz";
439         $badge->issuercontact = "issuer@example.com";
440         $badge->expiredate = null;
441         $badge->expireperiod = null;
442         $badge->type = BADGE_TYPE_COURSE;
443         $badge->courseid = $this->course->id;
444         $badge->messagesubject = "Test message subject for course badge";
445         $badge->message = "Test message body for course badge";
446         $badge->attachment = 1;
447         $badge->notification = 0;
448         $badge->status = BADGE_STATUS_ACTIVE;
449         $badge->version = "Version $i";
450         $badge->language = "en";
451         $badge->imagecaption = "Image caption";
452         $badge->imageauthorname = "Image author's name";
453         $badge->imageauthoremail = "author@example.com";
454         $badge->imageauthorname = "Image author's name";
456         $badgeid = $DB->insert_record('badge', $badge, true);
457         $badges[$badgeid] = new badge($badgeid);
458         $badges[$badgeid]->issue($this->user->id, true);
460         // With coursebadges off, we should only get the site badge.
461         set_config('badges_allowcoursebadges', false);
462         $result = badges_get_user_badges($this->user->id);
463         $this->assertCount(1, $result);
465         // With it on, we should get both.
466         set_config('badges_allowcoursebadges', true);
467         $result = badges_get_user_badges($this->user->id);
468         $this->assertCount(2, $result);
470     }
472     public function data_for_message_from_template() {
473         return array(
474             array(
475                 'This is a message with no variables',
476                 array(), // no params
477                 'This is a message with no variables'
478             ),
479             array(
480                 'This is a message with %amissing% variables',
481                 array(), // no params
482                 'This is a message with %amissing% variables'
483             ),
484             array(
485                 'This is a message with %one% variable',
486                 array('one' => 'a single'),
487                 'This is a message with a single variable'
488             ),
489             array(
490                 'This is a message with %one% %two% %three% variables',
491                 array('one' => 'more', 'two' => 'than', 'three' => 'one'),
492                 'This is a message with more than one variables'
493             ),
494             array(
495                 'This is a message with %three% %two% %one%',
496                 array('one' => 'variables', 'two' => 'ordered', 'three' => 'randomly'),
497                 'This is a message with randomly ordered variables'
498             ),
499             array(
500                 'This is a message with %repeated% %one% %repeated% of variables',
501                 array('one' => 'and', 'repeated' => 'lots'),
502                 'This is a message with lots and lots of variables'
503             ),
504         );
505     }
507     /**
508      * @dataProvider data_for_message_from_template
509      */
510     public function test_badge_message_from_template($message, $params, $result) {
511         $this->assertEquals(badge_message_from_template($message, $params), $result);
512     }
514     /**
515      * Test badges observer when course module completion event id fired.
516      */
517     public function test_badges_observer_course_module_criteria_review() {
518         $this->preventResetByRollback(); // Messaging is not compatible with transactions.
519         $badge = new badge($this->coursebadge);
520         $this->assertFalse($badge->is_issued($this->user->id));
522         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
523         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
524         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_ACTIVITY, 'badgeid' => $badge->id));
525         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'module_'.$this->module->cmid => $this->module->cmid));
527         // Assert the badge will not be issued to the user as is.
528         $badge = new badge($this->coursebadge);
529         $badge->review_all_criteria();
530         $this->assertFalse($badge->is_issued($this->user->id));
532         // Set completion for forum activity.
533         $c = new completion_info($this->course);
534         $activities = $c->get_activities();
535         $this->assertEquals(1, count($activities));
536         $this->assertTrue(isset($activities[$this->module->cmid]));
537         $this->assertEquals($activities[$this->module->cmid]->name, $this->module->name);
539         $current = $c->get_data($activities[$this->module->cmid], false, $this->user->id);
540         $current->completionstate = COMPLETION_COMPLETE;
541         $current->timemodified = time();
542         $sink = $this->redirectEmails();
543         $c->internal_set_data($activities[$this->module->cmid], $current);
544         $this->assertCount(1, $sink->get_messages());
545         $sink->close();
547         // Check if badge is awarded.
548         $this->assertDebuggingCalled('Error baking badge image!');
549         $this->assertTrue($badge->is_issued($this->user->id));
550     }
552     /**
553      * Test badges observer when course_completed event is fired.
554      */
555     public function test_badges_observer_course_criteria_review() {
556         $this->preventResetByRollback(); // Messaging is not compatible with transactions.
557         $badge = new badge($this->coursebadge);
558         $this->assertFalse($badge->is_issued($this->user->id));
560         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
561         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
562         $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_COURSE, 'badgeid' => $badge->id));
563         $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'course_'.$this->course->id => $this->course->id));
565         $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
567         // Assert the badge will not be issued to the user as is.
568         $badge = new badge($this->coursebadge);
569         $badge->review_all_criteria();
570         $this->assertFalse($badge->is_issued($this->user->id));
572         // Mark course as complete.
573         $sink = $this->redirectMessages();
574         $ccompletion->mark_complete();
575         // Two messages are generated: One for the course completed and the other one for the badge awarded.
576         $messages = $sink->get_messages();
577         $this->assertCount(2, $messages);
578         $this->assertEquals('badgerecipientnotice', $messages[0]->eventtype);
579         $this->assertEquals('coursecompleted', $messages[1]->eventtype);
580         $sink->close();
582         // Check if badge is awarded.
583         $this->assertDebuggingCalled('Error baking badge image!');
584         $this->assertTrue($badge->is_issued($this->user->id));
585     }
587     /**
588      * Test badges observer when user_updated event is fired.
589      */
590     public function test_badges_observer_profile_criteria_review() {
591         global $CFG, $DB;
592         require_once($CFG->dirroot.'/user/profile/lib.php');
594         // Add a custom field of textarea type.
595         $customprofileid = $DB->insert_record('user_info_field', array(
596             'shortname' => 'newfield', 'name' => 'Description of new field', 'categoryid' => 1,
597             'datatype' => 'textarea'));
599         $this->preventResetByRollback(); // Messaging is not compatible with transactions.
600         $badge = new badge($this->coursebadge);
602         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
603         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
604         $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
605         $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address', 'field_aim' => 'aim',
606             'field_' . $customprofileid => $customprofileid));
608         // Assert the badge will not be issued to the user as is.
609         $badge = new badge($this->coursebadge);
610         $badge->review_all_criteria();
611         $this->assertFalse($badge->is_issued($this->user->id));
613         // Set the required fields and make sure the badge got issued.
614         $this->user->address = 'Test address';
615         $this->user->aim = '999999999';
616         $sink = $this->redirectEmails();
617         profile_save_data((object)array('id' => $this->user->id, 'profile_field_newfield' => 'X'));
618         user_update_user($this->user, false);
619         $this->assertCount(1, $sink->get_messages());
620         $sink->close();
621         // Check if badge is awarded.
622         $this->assertDebuggingCalled('Error baking badge image!');
623         $this->assertTrue($badge->is_issued($this->user->id));
624     }
626     /**
627      * Test badges observer when cohort_member_added event is fired.
628      */
629     public function test_badges_observer_cohort_criteria_review() {
630         global $CFG;
632         require_once("$CFG->dirroot/cohort/lib.php");
634         $cohort = $this->getDataGenerator()->create_cohort();
636         $this->preventResetByRollback(); // Messaging is not compatible with transactions.
637         $badge = new badge($this->badgeid);
638         $this->assertFalse($badge->is_issued($this->user->id));
640         // Set up the badge criteria.
641         $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
642         $criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
643         $criteriaoverall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_COHORT, 'badgeid' => $badge->id));
644         $criteriaoverall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'cohort_cohorts' => array('0' => $cohort->id)));
646         // Make the badge active.
647         $badge->set_status(BADGE_STATUS_ACTIVE);
649         // Add the user to the cohort.
650         cohort_add_member($cohort->id, $this->user->id);
652         // Verify that the badge was awarded.
653         $this->assertDebuggingCalled();
654         $this->assertTrue($badge->is_issued($this->user->id));
656     }
658     /**
659      * Test badges assertion generated when a badge is issued.
660      */
661     public function test_badges_assertion() {
662         $this->preventResetByRollback(); // Messaging is not compatible with transactions.
663         $badge = new badge($this->coursebadge);
664         $this->assertFalse($badge->is_issued($this->user->id));
666         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
667         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
668         $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
669         $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address'));
671         $this->user->address = 'Test address';
672         $sink = $this->redirectEmails();
673         user_update_user($this->user, false);
674         $this->assertCount(1, $sink->get_messages());
675         $sink->close();
676         // Check if badge is awarded.
677         $this->assertDebuggingCalled('Error baking badge image!');
678         $awards = $badge->get_awards();
679         $this->assertCount(1, $awards);
681         // Get assertion.
682         $award = reset($awards);
683         $assertion = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V1);
684         $testassertion = $this->assertion;
686         // Make sure JSON strings have the same structure.
687         $this->assertStringMatchesFormat($testassertion->badge, json_encode($assertion->get_badge_assertion()));
688         $this->assertStringMatchesFormat($testassertion->class, json_encode($assertion->get_badge_class()));
689         $this->assertStringMatchesFormat($testassertion->issuer, json_encode($assertion->get_issuer()));
691         // Test Openbadge specification version 2.
692         // Get assertion version 2.
693         $award = reset($awards);
694         $assertion2 = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V2);
695         $testassertion2 = $this->assertion2;
697         // Make sure JSON strings have the same structure.
698         $this->assertStringMatchesFormat($testassertion2->badge, json_encode($assertion2->get_badge_assertion()));
699         $this->assertStringMatchesFormat($testassertion2->class, json_encode($assertion2->get_badge_class()));
700         $this->assertStringMatchesFormat($testassertion2->issuer, json_encode($assertion2->get_issuer()));
701     }
703     /**
704      * Tests the core_badges_myprofile_navigation() function.
705      */
706     public function test_core_badges_myprofile_navigation() {
707         // Set up the test.
708         $tree = new \core_user\output\myprofile\tree();
709         $this->setAdminUser();
710         $badge = new badge($this->badgeid);
711         $badge->issue($this->user->id, true);
712         $iscurrentuser = true;
713         $course = null;
715         // Enable badges.
716         set_config('enablebadges', true);
718         // Check the node tree is correct.
719         core_badges_myprofile_navigation($tree, $this->user, $iscurrentuser, $course);
720         $reflector = new ReflectionObject($tree);
721         $nodes = $reflector->getProperty('nodes');
722         $nodes->setAccessible(true);
723         $this->assertArrayHasKey('localbadges', $nodes->getValue($tree));
724     }
726     /**
727      * Tests the core_badges_myprofile_navigation() function with badges disabled..
728      */
729     public function test_core_badges_myprofile_navigation_badges_disabled() {
730         // Set up the test.
731         $tree = new \core_user\output\myprofile\tree();
732         $this->setAdminUser();
733         $badge = new badge($this->badgeid);
734         $badge->issue($this->user->id, true);
735         $iscurrentuser = false;
736         $course = null;
738         // Disable badges.
739         set_config('enablebadges', false);
741         // Check the node tree is correct.
742         core_badges_myprofile_navigation($tree, $this->user, $iscurrentuser, $course);
743         $reflector = new ReflectionObject($tree);
744         $nodes = $reflector->getProperty('nodes');
745         $nodes->setAccessible(true);
746         $this->assertArrayNotHasKey('localbadges', $nodes->getValue($tree));
747     }
749     /**
750      * Tests the core_badges_myprofile_navigation() function with a course badge.
751      */
752     public function test_core_badges_myprofile_navigation_with_course_badge() {
753         // Set up the test.
754         $tree = new \core_user\output\myprofile\tree();
755         $this->setAdminUser();
756         $badge = new badge($this->coursebadge);
757         $badge->issue($this->user->id, true);
758         $iscurrentuser = false;
760         // Check the node tree is correct.
761         core_badges_myprofile_navigation($tree, $this->user, $iscurrentuser, $this->course);
762         $reflector = new ReflectionObject($tree);
763         $nodes = $reflector->getProperty('nodes');
764         $nodes->setAccessible(true);
765         $this->assertArrayHasKey('localbadges', $nodes->getValue($tree));
766     }
768     /**
769      * Test insert and update endorsement with a site badge.
770      */
771     public function test_badge_endorsement() {
772         $badge = new badge($this->badgeid);
774         // Insert Endorsement.
775         $endorsement = new stdClass();
776         $endorsement->badgeid = $this->badgeid;
777         $endorsement->issuername = "Issuer 123";
778         $endorsement->issueremail = "issuer123@email.com";
779         $endorsement->issuerurl = "https://example.org/issuer-123";
780         $endorsement->dateissued = 1524567747;
781         $endorsement->claimid = "https://example.org/robotics-badge.json";
782         $endorsement->claimcomment = "Test endorser comment";
784         $badge->save_endorsement($endorsement);
785         $endorsement1 = $badge->get_endorsement();
786         $this->assertEquals($endorsement->badgeid, $endorsement1->badgeid);
787         $this->assertEquals($endorsement->issuername, $endorsement1->issuername);
788         $this->assertEquals($endorsement->issueremail, $endorsement1->issueremail);
789         $this->assertEquals($endorsement->issuerurl, $endorsement1->issuerurl);
790         $this->assertEquals($endorsement->dateissued, $endorsement1->dateissued);
791         $this->assertEquals($endorsement->claimid, $endorsement1->claimid);
792         $this->assertEquals($endorsement->claimcomment, $endorsement1->claimcomment);
794         // Update Endorsement.
795         $endorsement1->issuername = "Issuer update";
796         $badge->save_endorsement($endorsement1);
797         $endorsement2 = $badge->get_endorsement();
798         $this->assertEquals($endorsement1->id, $endorsement2->id);
799         $this->assertEquals($endorsement1->issuername, $endorsement2->issuername);
800     }
802     /**
803      * Test insert and delete related badge with a site badge.
804      */
805     public function test_badge_related() {
806         $badge = new badge($this->badgeid);
807         $newid1 = $badge->make_clone();
808         $newid2 = $badge->make_clone();
809         $newid3 = $badge->make_clone();
811         // Insert an related badge.
812         $badge->add_related_badges([$newid1, $newid2, $newid3]);
813         $this->assertCount(3, $badge->get_related_badges());
815         // Only get related is active.
816         $clonedbage1 = new badge($newid1);
817         $clonedbage1->status = BADGE_STATUS_ACTIVE;
818         $clonedbage1->save();
819         $this->assertCount(1, $badge->get_related_badges(true));
821         // Delete an related badge.
822         $badge->delete_related_badge($newid2);
823         $this->assertCount(2, $badge->get_related_badges());
824     }
826     /**
827      * Test insert, update, delete alignment with a site badge.
828      */
829     public function test_alignments() {
830         $badge = new badge($this->badgeid);
832         // Insert a alignment.
833         $alignment1 = new stdClass();
834         $alignment1->badgeid = $this->badgeid;
835         $alignment1->targetname = 'CCSS.ELA-Literacy.RST.11-12.3';
836         $alignment1->targeturl = 'http://www.corestandards.org/ELA-Literacy/RST/11-12/3';
837         $alignment1->targetdescription = 'Test target description';
838         $alignment1->targetframework = 'CCSS.RST.11-12.3';
839         $alignment1->targetcode = 'CCSS.RST.11-12.3';
840         $alignment2 = clone $alignment1;
841         $newid1 = $badge->save_alignment($alignment1);
842         $newid2 = $badge->save_alignment($alignment2);
843         $alignments1 = $badge->get_alignments();
844         $this->assertCount(2, $alignments1);
846         $this->assertEquals($alignment1->badgeid, $alignments1[$newid1]->badgeid);
847         $this->assertEquals($alignment1->targetname, $alignments1[$newid1]->targetname);
848         $this->assertEquals($alignment1->targeturl, $alignments1[$newid1]->targeturl);
849         $this->assertEquals($alignment1->targetdescription, $alignments1[$newid1]->targetdescription);
850         $this->assertEquals($alignment1->targetframework, $alignments1[$newid1]->targetframework);
851         $this->assertEquals($alignment1->targetcode, $alignments1[$newid1]->targetcode);
853         // Update aligment.
854         $alignments1[$newid1]->targetname = 'CCSS.ELA-Literacy.RST.11-12.3 update';
855         $badge->save_alignment($alignments1[$newid1], $alignments1[$newid1]->id);
856         $alignments2 = $badge->get_alignments();
857         $this->assertEquals($alignments1[$newid1]->id, $alignments2[$newid1]->id);
858         $this->assertEquals($alignments1[$newid1]->targetname, $alignments2[$newid1]->targetname);
860         // Delete alignment.
861         $badge->delete_alignment($alignments1[$newid2]->id);
862         $this->assertCount(1, $badge->get_alignments());
863     }
865     /**
866      * Test badges_delete_site_backpack().
867      *
868      */
869     public function test_badges_delete_site_backpack(): void {
870         global $DB;
872         $this->setAdminUser();
874         // Create one backpack.
875         $total = $DB->count_records('badge_external_backpack');
876         $this->assertEquals(1, $total);
878         $data = new \stdClass();
879         $data->apiversion = OPEN_BADGES_V2P1;
880         $data->backpackapiurl = 'https://dc.imsglobal.org/obchost/ims/ob/v2p1';
881         $data->backpackweburl = 'https://dc.imsglobal.org';
882         badges_create_site_backpack($data);
883         $backpack = $DB->get_record('badge_external_backpack', ['backpackweburl' => $data->backpackweburl]);
884         $user1 = $this->getDataGenerator()->create_user();
885         $user2 = $this->getDataGenerator()->create_user();
886         // User1 is connected to the backpack to be removed and has 2 collections.
887         $backpackuser1 = helper::create_fake_backpack(['userid' => $user1->id, 'externalbackpackid' => $backpack->id]);
888         helper::create_fake_backpack_collection(['backpackid' => $backpackuser1->id]);
889         helper::create_fake_backpack_collection(['backpackid' => $backpackuser1->id]);
890         // User2 is connected to a different backpack and has 1 collection.
891         $backpackuser2 = helper::create_fake_backpack(['userid' => $user2->id]);
892         helper::create_fake_backpack_collection(['backpackid' => $backpackuser2->id]);
894         $total = $DB->count_records('badge_external_backpack');
895         $this->assertEquals(2, $total);
896         $total = $DB->count_records('badge_backpack');
897         $this->assertEquals(2, $total);
898         $total = $DB->count_records('badge_external');
899         $this->assertEquals(3, $total);
901         // Remove the backpack created previously.
902         $result = badges_delete_site_backpack($backpack->id);
903         $this->assertTrue($result);
905         $total = $DB->count_records('badge_external_backpack');
906         $this->assertEquals(1, $total);
908         $total = $DB->count_records('badge_backpack');
909         $this->assertEquals(1, $total);
911         $total = $DB->count_records('badge_external');
912         $this->assertEquals(1, $total);
914         // Try to remove an non-existent backpack.
915         $result = badges_delete_site_backpack($backpack->id);
916         $this->assertFalse($result);
917     }
919     /**
920      * Test to validate badges_save_backpack_credentials.
921      *
922      * @dataProvider save_backpack_credentials_provider
923      * @param  bool $addbackpack True if backpack data has to be created; false otherwise (empty data will be used then).
924      * @param  string|null  $mail  Backpack mail address.
925      * @param  string|null  $password  Backpack password.
926      */
927     public function test_save_backpack_credentials(bool $addbackpack = true, ?string $mail = null, ?string $password = null) {
928         global $DB;
930         $this->resetAfterTest();
931         $this->setAdminUser();
933         $data = [];
934         if ($addbackpack) {
935             $data = new \stdClass();
936             $data->apiversion = OPEN_BADGES_V2P1;
937             $data->backpackapiurl = 'https://dc.imsglobal.org/obchost/ims/ob/v2p1';
938             $data->backpackweburl = 'https://dc.imsglobal.org';
939             badges_create_site_backpack($data);
940             $backpack = $DB->get_record('badge_external_backpack', ['backpackweburl' => $data->backpackweburl]);
941             $user = $this->getDataGenerator()->create_user();
943             $data = [
944                 'externalbackpackid' => $backpack->id,
945                 'userid' => $user->id,
946             ];
948             if (!empty($mail)) {
949                 $data['backpackemail'] = $mail;
950             }
951             if (!empty($password)) {
952                 $data['password'] = $password;
953             }
954         }
956         $return = badges_save_backpack_credentials((object) $data);
957         if (array_key_exists('userid', $data)) {
958             $record = $DB->get_record('badge_backpack', ['userid' => $user->id]);
959         } else {
960             $record = $DB->get_records('badge_backpack');
961         }
963         if (!empty($mail) && !empty($password)) {
964             // The backpack credentials are created if the given information is right.
965             $this->assertNotEmpty($record);
966             $this->assertEquals($data['externalbackpackid'], $return);
967         } else if ($addbackpack) {
968             // If no email and password are given, no backpack is created/modified.
969             $this->assertEmpty($record);
970             $this->assertEquals($data['externalbackpackid'], $return);
971         } else {
972             // There weren't fields to add to the backpack so no DB change is expected.
973             $this->assertEmpty($record);
974             $this->assertEquals(0, $return);
975         }
977         // Confirm the existing backpack credential can be updated (if it has been created).
978         if (!empty($record)) {
979             $data['backpackemail'] = 'modified_' . $mail;
980             $data['id'] = $record->id;
981             $return = badges_save_backpack_credentials((object) $data);
982             $record = $DB->get_record('badge_backpack', ['userid' => $user->id]);
984             $this->assertNotEmpty($record);
985             $this->assertEquals($data['backpackemail'], $record->email);
986             $this->assertEquals($data['externalbackpackid'], $return);
987         }
988     }
990     /**
991      * Data provider for test_create_backpack_credentials().
992      *
993      * @return array
994      */
995     public function save_backpack_credentials_provider(): array {
996         return [
997             'Empty fields' => [
998                 false,
999             ],
1000             'No backpack mail or password are defined' => [
1001                 true,
1002             ],
1003             'Both backpack mail and password are defined' => [
1004                 true, 'test@test.com', '1234',
1005             ],
1006             'Only backpack mail is defined (no password is given)' => [
1007                 true, 'test@test.com', null,
1008             ],
1009             'Only backpack password is defined (no mail is given)' => [
1010                 true, null, '1234'
1011             ],
1012         ];
1013     }
1015     /**
1016      * Test badges_save_external_backpack.
1017      *
1018      * @dataProvider badges_save_external_backpack_provider
1019      * @param  array $data  Backpack data to save.
1020      * @param  bool $adduser True if a real user has to be used for creating the backpack; false otherwise.
1021      * @param  bool $duplicates True if duplicates has to be tested too; false otherwise.
1022      */
1023     public function test_badges_save_external_backpack(array $data, bool $adduser, bool $duplicates) {
1024         global $DB;
1026         $this->resetAfterTest();
1028         $userid = 0;
1029         if ($adduser) {
1030             $user = $this->getDataGenerator()->create_user();
1031             $userid = $user->id;
1032             $data['userid'] = $user->id;
1033         }
1035         $result = badges_save_external_backpack((object) $data);
1036         $this->assertNotEquals(0, $result);
1037         $record = $DB->get_record('badge_external_backpack', ['id' => $result]);
1038         $this->assertEquals($record->backpackweburl, $data['backpackweburl']);
1039         $this->assertEquals($record->backpackapiurl, $data['backpackapiurl']);
1041         $record = $DB->get_record('badge_backpack', ['externalbackpackid' => $result]);
1042         if (!array_key_exists('backpackemail', $data) && !array_key_exists('password', $data)) {
1043             $this->assertEmpty($record);
1044             $total = $DB->count_records('badge_backpack');
1045             $this->assertEquals(0, $total);
1046         } else {
1047             $this->assertNotEmpty($record);
1048             $this->assertEquals($record->userid, $userid);
1049         }
1051         if ($duplicates) {
1052             // We shouldn't be able to insert multiple external_backpacks with the same values.
1053             $this->expectException('dml_write_exception');
1054             $result = badges_save_external_backpack((object)$data);
1055         }
1056     }
1058     /**
1059      * Provider for test_badges_save_external_backpack
1060      *
1061      * @return array
1062      */
1063     public function badges_save_external_backpack_provider() {
1064         $data = [
1065             'apiversion' => 2,
1066             'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1067             'backpackweburl' => 'https://ca.badgr.io',
1068         ];
1069         return [
1070             'Test without user and auth details. Check duplicates too' => [
1071                 'data' => $data,
1072                 'adduser' => false,
1073                 'duplicates' => true,
1074             ],
1075             'Test without user and auth details. No duplicates' => [
1076                 'data' => $data,
1077                 'adduser' => false,
1078                 'duplicates' => false,
1079             ],
1080             'Test with user and without auth details' => [
1081                 'data' => $data,
1082                 'adduser' => true,
1083                 'duplicates' => false,
1084             ],
1085             'Test with user and without auth details. Check duplicates too' => [
1086                 'data' => $data,
1087                 'adduser' => true,
1088                 'duplicates' => true,
1089             ],
1090             'Test with empty backpackemail, password and id' => [
1091                 'data' => array_merge($data, [
1092                     'backpackemail' => '',
1093                     'password' => '',
1094                     'id' => 0,
1095                 ]),
1096                 'adduser' => false,
1097                 'duplicates' => false,
1098             ],
1099             'Test with empty backpackemail, password and id but with user' => [
1100                 'data' => array_merge($data, [
1101                     'backpackemail' => '',
1102                     'password' => '',
1103                     'id' => 0,
1104                 ]),
1105                 'adduser' => true,
1106                 'duplicates' => false,
1107             ],
1108             'Test with auth details but without user' => [
1109                 'data' => array_merge($data, [
1110                     'backpackemail' => 'test@test.com',
1111                     'password' => 'test',
1112                 ]),
1113                 'adduser' => false,
1114                 'duplicates' => false,
1115             ],
1116             'Test with auth details and user' => [
1117                 'data' => array_merge($data, [
1118                     'backpackemail' => 'test@test.com',
1119                     'password' => 'test',
1120                 ]),
1121                 'adduser' => true,
1122                 'duplicates' => false,
1123             ],
1124         ];
1125     }
1127     /**
1128      * Test backpack creation/update with auth details provided
1129      *
1130      * @param boolean $isadmin
1131      * @param boolean $updatetest
1132      * @dataProvider badges_create_site_backpack_provider
1133      */
1134     public function test_badges_create_site_backpack($isadmin, $updatetest) {
1135         global $DB;
1136         $this->resetAfterTest();
1138         $data = [
1139             'apiversion' => 2,
1140             'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1141             'backpackweburl' => 'https://ca.badgr.io',
1142         ];
1144         $data['backpackemail'] = 'test@test.com';
1145         $data['password'] = 'test';
1146         if ($isadmin || $updatetest) {
1147             $this->setAdminUser();
1148             $backpack = badges_create_site_backpack((object) $data);
1149         }
1151         if ($isadmin) {
1152             if ($updatetest) {
1153                 $record = $DB->get_record('badge_backpack', ['userid' => 0]);
1154                 $data['badgebackpack'] = $record->id;
1155                 $data['backpackapiurl'] = 'https://api.ca.badgr.io/v3';
1156                 badges_update_site_backpack($backpack, (object)$data);
1157             }
1158             $record = $DB->get_record('badge_external_backpack', ['id' => $backpack]);
1159             $this->assertEquals($record->backpackweburl, $data['backpackweburl']);
1160             $this->assertEquals($record->backpackapiurl, $data['backpackapiurl']);
1161             $record = $DB->get_record('badge_backpack', ['userid' => 0]);
1162             $this->assertNotEmpty($record);
1163         } else {
1164             $user = $this->getDataGenerator()->create_user();
1165             $this->setUser($user);
1166             $this->expectException('required_capability_exception');
1167             if ($updatetest) {
1168                 $result = badges_update_site_backpack($backpack, (object) $data);
1169             } else {
1170                 $result = badges_create_site_backpack((object)$data);
1171             }
1172         }
1173     }
1175     /**
1176      * Provider for test_badges_(create/update)_site_backpack
1177      */
1178     public function badges_create_site_backpack_provider() {
1179         return [
1180             "Test as admin user - creation test" => [true, true],
1181             "Test as admin user - update test" => [true, false],
1182             "Test as normal user - creation test" => [false, true],
1183             "Test as normal user - update test" => [false, false],
1184         ];
1185     }
1187     /**
1188      * Test the badges_open_badges_backpack_api with different backpacks
1189      */
1190     public function test_badges_open_badges_backpack_api() {
1191         $this->resetAfterTest();
1193         $data = [
1194             'apiversion' => 2,
1195             'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1196             'backpackweburl' => 'https://ca.badgr.io',
1197         ];
1199         // Given a complete set of unique data, a new backpack and auth records should exist in the tables.
1200         $data['backpackemail'] = 'test@test.com';
1201         $data['password'] = 'test';
1202         $backpack1 = badges_save_external_backpack((object) $data);
1203         $data['backpackweburl'] = 'https://eu.badgr.io';
1204         $data['backpackapiurl'] = 'https://api.eu.badgr.io/v2';
1205         $data['apiversion'] = 2.1;
1206         $backpack2 = badges_save_external_backpack((object) $data);
1208         set_config('badges_site_backpack', $backpack2);
1209         // The default response should check the default site backpack api version.
1210         $this->assertEquals(2.1, badges_open_badges_backpack_api());
1211         // Check the api version for the other backpack created.
1212         $this->assertEquals(2, badges_open_badges_backpack_api($backpack1));
1213         $this->assertEquals(2.1, badges_open_badges_backpack_api($backpack2));
1214     }
1216     /**
1217      * Test the badges_get_site_backpack function
1218      */
1219     public function test_badges_get_site_backpack() {
1220         $this->resetAfterTest();
1221         $user = $this->getDataGenerator()->create_user();
1222         $data = [
1223             'apiversion' => '2',
1224             'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1225             'backpackweburl' => 'https://ca.badgr.io',
1226         ];
1227         $backpack1 = badges_save_external_backpack((object) $data);
1228         $data2 = array_merge($data, [
1229             'backpackapiurl' => 'https://api.eu.badgr.io/v2',
1230             'backpackweburl' => 'https://eu.badgr.io',
1231             'backpackemail' => 'test@test.com',
1232             'password' => 'test',
1233         ]);
1234         $backpack2 = badges_save_external_backpack((object) $data2);
1235         $data3 = array_merge($data2, [
1236             'userid' => $user->id,
1237             'externalbackpackid' => $backpack2,
1238             'backpackemail' => 'test2@test.com'
1239         ]);
1240         // In the following case, the id returned below equals backpack2. So we aren't storing it.
1241         badges_save_backpack_credentials((object) $data3);
1242         unset($data3['userid']);
1244         // Get a site back based on the id returned from creation and no user id provided.
1245         $this->assertEquals($data, array_intersect($data, (array) badges_get_site_backpack($backpack1)));
1246         $this->assertEquals($data2, array_intersect($data2, (array) badges_get_site_backpack($backpack2)));
1247         $this->assertEquals($data2, array_intersect($data2, (array) badges_get_site_backpack($backpack2, 0)));
1248         $this->assertEquals($data3, array_intersect($data3, (array) badges_get_site_backpack($backpack2, $user->id)));
1250         // Non-existent user backpack should return only configuration details and not auth details.
1251         $userbackpack = badges_get_site_backpack($backpack1, $user->id);
1252         $this->assertNull($userbackpack->badgebackpack);
1253         $this->assertNull($userbackpack->password);
1254         $this->assertNull($userbackpack->backpackemail);
1255     }
1257     /**
1258      * Test the badges_get_user_backpack function
1259      */
1260     public function test_badges_get_user_backpack() {
1261         $this->resetAfterTest();
1262         $user = $this->getDataGenerator()->create_user();
1263         $data = [
1264             'apiversion' => '2',
1265             'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1266             'backpackweburl' => 'https://ca.badgr.io',
1267         ];
1268         $backpack1 = badges_save_external_backpack((object) $data);
1269         $data2 = array_merge($data, [
1270             'backpackapiurl' => 'https://api.eu.badgr.io/v2',
1271             'backpackweburl' => 'https://eu.badgr.io',
1272             'backpackemail' => 'test@test.com',
1273             'password' => 'test',
1274         ]);
1275         $backpack2 = badges_save_external_backpack((object) $data2);
1276         $data3 = array_merge($data2, [
1277             'userid' => $user->id,
1278             'externalbackpackid' => $backpack2,
1279             'backpackemail' => 'test2@test.com'
1280         ]);
1281         // In the following case, the id returned below equals backpack2. So we aren't storing it.
1282         badges_save_backpack_credentials((object) $data3);
1283         unset($data3['userid']);
1285         // Currently logged in as admin.
1286         $this->assertEquals($data2, array_intersect($data2, (array) badges_get_user_backpack()));
1287         $this->assertEquals($data2, array_intersect($data2, (array) badges_get_user_backpack(0)));
1288         $this->assertEquals($data3, array_intersect($data3, (array) badges_get_user_backpack($user->id)));
1290         // Non-existent user backpack should return nothing.
1291         $this->assertFalse(badges_get_user_backpack($backpack1, $user->id));
1293         // Login as user.
1294         $this->setUser($user);
1295         $this->assertEquals($data3, array_intersect($data3, (array) badges_get_user_backpack()));
1296     }
1298     /**
1299      * Test the badges_get_site_primary_backpack function
1300      *
1301      * @param boolean $withauth Testing with authentication or not.
1302      * @dataProvider badges_get_site_primary_backpack_provider
1303      */
1304     public function test_badges_get_site_primary_backpack($withauth) {
1305         $data = [
1306             'apiversion' => '2',
1307             'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1308             'backpackweburl' => 'https://ca.badgr.io',
1309         ];
1310         if ($withauth) {
1311             $data = array_merge($data, [
1312                 'backpackemail' => 'test@test.com',
1313                 'password' => 'test',
1314             ]);
1315         }
1316         $backpack = badges_save_external_backpack((object) $data);
1318         set_config('badges_site_backpack', $backpack);
1319         $sitebackpack = badges_get_site_primary_backpack();
1320         $this->assertEquals($backpack, $sitebackpack->id);
1322         if ($withauth) {
1323             $this->assertEquals($data, array_intersect($data, (array) $sitebackpack));
1324             $this->assertEquals($data['password'], $sitebackpack->password);
1325             $this->assertEquals($data['backpackemail'], $sitebackpack->backpackemail);
1326         } else {
1327             $this->assertNull($sitebackpack->badgebackpack);
1328             $this->assertNull($sitebackpack->password);
1329             $this->assertNull($sitebackpack->backpackemail);
1330         }
1331     }
1333     /**
1334      * Test the test_badges_get_site_primary_backpack function.
1335      *
1336      * @return array
1337      */
1338     public function badges_get_site_primary_backpack_provider() {
1339         return [
1340             "Test with auth details" => [true],
1341             "Test without auth details" => [false],
1342         ];
1343     }
1345     /**
1346      * Test the Badgr URL generator function
1347      *
1348      * @param mixed $type Type corresponding to the badge entites
1349      * @param string $expected Expected string result
1350      * @dataProvider badgr_open_url_generator
1351      */
1352     public function test_badges_generate_badgr_open_url($type, $expected) {
1353         $data = [
1354             'apiversion' => '2',
1355             'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1356             'backpackweburl' => 'https://ca.badgr.io',
1357             'backpackemail' => 'test@test.com',
1358             'password' => 'test',
1359         ];
1360         $backpack2 = badges_save_external_backpack((object) $data);
1361         $backpack = badges_get_site_backpack($backpack2);
1362         $this->assertEquals($expected, badges_generate_badgr_open_url($backpack, $type, 123455));
1363     }
1365     /**
1366      * Data provider for test_badges_generate_badgr_open_url
1367      * @return array
1368      */
1369     public function badgr_open_url_generator() {
1370         return [
1371             'Badgr Assertion URL test' => [
1372                 OPEN_BADGES_V2_TYPE_ASSERTION, "https://api.ca.badgr.io/public/assertions/123455"
1373             ],
1374             'Badgr Issuer URL test' => [
1375                 OPEN_BADGES_V2_TYPE_ISSUER, "https://api.ca.badgr.io/public/issuers/123455"
1376             ],
1377             'Badgr Badge URL test' => [
1378                 OPEN_BADGES_V2_TYPE_BADGE, "https://api.ca.badgr.io/public/badges/123455"
1379             ]
1380         ];
1381     }
1383     /**
1384      * Test badges_external_get_mapping function
1385      *
1386      * @param int $internalid The internal id of the mapping
1387      * @param int $externalid The external / remote ref to the mapping
1388      * @param mixed $expected The expected result from the function
1389      * @param string|null $field The field we are passing to the function. Null if we don't want to pass anything.ss
1390      *
1391      * @dataProvider badges_external_get_mapping_provider
1392      */
1393     public function test_badges_external_get_mapping($internalid, $externalid, $expected, $field = null) {
1394         $data = [
1395             'apiversion' => '2',
1396             'backpackapiurl' => 'https://api.ca.badgr.io/v2',
1397             'backpackweburl' => 'https://ca.badgr.io',
1398             'backpackemail' => 'test@test.com',
1399             'password' => 'test',
1400         ];
1401         $backpack2 = badges_save_external_backpack((object) $data);
1402         badges_external_create_mapping($backpack2, OPEN_BADGES_V2_TYPE_BADGE, $internalid, $externalid);
1403         $expected = $expected == "id" ? $backpack2 : $expected;
1404         if ($field) {
1405             $this->assertEquals($expected, badges_external_get_mapping($backpack2, OPEN_BADGES_V2_TYPE_BADGE, $internalid, $field));
1406         } else {
1407             $this->assertEquals($expected, badges_external_get_mapping($backpack2, OPEN_BADGES_V2_TYPE_BADGE, $internalid));
1408         }
1409     }
1411     /**
1412      * Data provider for badges_external_get_mapping_provider
1413      *
1414      * @return array
1415      */
1416     public function badges_external_get_mapping_provider() {
1417         return [
1418             "Get the site backpack value" => [
1419                 1234, 4321, 'id', 'sitebackpackid'
1420             ],
1421             "Get the type of the mapping" => [
1422                 1234, 4321, OPEN_BADGES_V2_TYPE_BADGE, 'type'
1423             ],
1424             "Get the externalid of the mapping" => [
1425                 1234, 4321, 4321, 'externalid'
1426             ],
1427             "Get the externalid of the mapping without providing a param" => [
1428                 1234, 4321, 4321, null
1429             ],
1430             "Get the internalid of the mapping" => [
1431                 1234, 4321, 1234, 'internalid'
1432             ]
1433         ];
1434     }