Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
namespace enrol_lti\local\ltiadvantage\repository;
18
use enrol_lti\local\ltiadvantage\entity\application_registration;
19
use enrol_lti\local\ltiadvantage\entity\user;
20
 
21
/**
22
 * Tests for user_repository objects.
23
 *
24
 * @package enrol_lti
25
 * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
26
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27
 * @coversDefaultClass \enrol_lti\local\ltiadvantage\repository\user_repository
28
 */
1441 ariadna 29
final class user_repository_test extends \advanced_testcase {
1 efrain 30
    /**
31
     * Helper to generate a new user instance.
32
     *
33
     * @param int $mockresourceid used to spoof a published resource, to which this user is associated.
34
     * @param array $userfields user information like city, timezone which would normally come from the tool configuration.
35
     * @return user a user instance
36
     */
37
    protected function generate_user(int $mockresourceid = 1, array $userfields = []): user {
38
        global $CFG;
39
        $registration = application_registration::create(
40
            'Test',
41
            'a2c94a2c94',
42
            new \moodle_url('http://lms.example.org'),
43
            'clientid_123',
44
            new \moodle_url('https://example.org/authrequesturl'),
45
            new \moodle_url('https://example.org/jwksurl'),
46
            new \moodle_url('https://example.org/accesstokenurl')
47
        );
48
        $registrationrepo = new application_registration_repository();
49
        $createdregistration = $registrationrepo->save($registration);
50
 
51
        $deployment = $createdregistration->add_tool_deployment('Deployment 1', 'DeployID123');
52
        $deploymentrepo = new deployment_repository();
53
        $saveddeployment = $deploymentrepo->save($deployment);
54
 
55
        $contextrepo = new context_repository();
56
        $context = $saveddeployment->add_context(
57
            'CTX123',
58
            ['http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection']
59
        );
60
        $savedcontext = $contextrepo->save($context);
61
 
62
        $resourcelinkrepo = new resource_link_repository();
63
        $resourcelink = $saveddeployment->add_resource_link('resourcelinkid_123', $mockresourceid,
64
            $savedcontext->get_id());
65
        $savedresourcelink = $resourcelinkrepo->save($resourcelink);
66
 
67
        // Create a user using the DB defaults to simulate what would have occurred during an auth_lti user auth.
68
        $user = $this->getDataGenerator()->create_user([
69
            'city' => '',
70
            'country' => '',
71
            'institution' => '',
72
            'timezone' => '99',
73
            'maildisplay' => 2,
74
            'lang' => 'en'
75
        ]);
76
 
77
        $userdefaultvalues = [
78
            'lang' => $CFG->lang,
79
            'city' => '',
80
            'country' => '',
81
            'institution' => '',
82
            'timezone' => '99',
83
            'maildisplay' => 2
84
        ];
85
        if (empty($userfields)) {
86
            // If userfields is omitted, assume the tool default configuration values (as if 'User default values' are unchanged).
87
            $userfields = $userdefaultvalues;
88
        } else {
89
            // If they have been provided, merge and override the defaults.
90
            $userfields = array_merge($userdefaultvalues, $userfields);
91
        }
92
        $ltiuser = $savedresourcelink->add_user(
93
            $user->id,
94
            'source-id-123',
95
            ...array_values($userfields)
96
        );
97
 
98
        $ltiuser->set_lastgrade(67.33333333);
99
 
100
        return $ltiuser;
101
    }
102
 
103
    /**
104
     * Helper to assert that all the key elements of two users (i.e. excluding id) are equal.
105
     *
106
     * @param user $expected the user whose values are deemed correct.
107
     * @param user $check the user to check.
108
     * @param bool $checkresourcelink whether or not to confirm the resource link value matches too.
109
     */
110
    protected function assert_same_user_values(user $expected, user $check, bool $checkresourcelink = false): void {
111
        $this->assertEquals($expected->get_deploymentid(), $check->get_deploymentid());
112
        $this->assertEquals($expected->get_city(), $check->get_city());
113
        $this->assertEquals($expected->get_country(), $check->get_country());
114
        $this->assertEquals($expected->get_institution(), $check->get_institution());
115
        $this->assertEquals($expected->get_timezone(), $check->get_timezone());
116
        $this->assertEquals($expected->get_maildisplay(), $check->get_maildisplay());
117
        $this->assertEquals($expected->get_lang(), $check->get_lang());
118
        if ($checkresourcelink) {
119
            $this->assertEquals($expected->get_resourcelinkid(), $check->get_resourcelinkid());
120
        }
121
    }
122
 
123
    /**
124
     * Helper to assert that all the key elements of a user are present in the DB.
125
     *
126
     * @param user $expected the user whose values are deemed correct.
127
     */
128
    protected function assert_user_db_values(user $expected) {
129
        global $DB;
130
        $sql = "SELECT u.username, u.firstname, u.lastname, u.email, u.city, u.country, u.institution, u.timezone,
131
                       u.maildisplay, u.mnethostid, u.confirmed, u.lang, u.auth
132
                  FROM {enrol_lti_users} lu
133
                  JOIN {user} u
134
                    ON (lu.userid = u.id)
135
                 WHERE lu.id = :id";
136
        $userrecord = $DB->get_record_sql($sql, ['id' => $expected->get_id()]);
137
        $this->assertEquals($expected->get_city(), $userrecord->city);
138
        $this->assertEquals($expected->get_country(), $userrecord->country);
139
        $this->assertEquals($expected->get_institution(), $userrecord->institution);
140
        $this->assertEquals($expected->get_timezone(), $userrecord->timezone);
141
        $this->assertEquals($expected->get_maildisplay(), $userrecord->maildisplay);
142
        $this->assertEquals($expected->get_lang(), $userrecord->lang);
143
 
144
        $ltiuserrecord = $DB->get_record('enrol_lti_users', ['id' => $expected->get_id()]);
145
        $this->assertEquals($expected->get_id(), $ltiuserrecord->id);
146
        $this->assertEquals($expected->get_sourceid(), $ltiuserrecord->sourceid);
147
        $this->assertEquals($expected->get_resourceid(), $ltiuserrecord->toolid);
148
        $this->assertEquals($expected->get_lastgrade(), $ltiuserrecord->lastgrade);
149
 
150
        if ($expected->get_resourcelinkid()) {
151
            $sql = "SELECT rl.id
152
                      FROM {enrol_lti_users} lu
153
                      JOIN {enrol_lti_user_resource_link} rlj
154
                        ON (lu.id = rlj.ltiuserid)
155
                      JOIN {enrol_lti_resource_link} rl
156
                        ON (rl.id = rlj.resourcelinkid)
157
                     WHERE lu.id = :id";
158
            $resourcelinkrecord = $DB->get_record_sql($sql, ['id' => $expected->get_id()]);
159
            $this->assertEquals($expected->get_resourcelinkid(), $resourcelinkrecord->id);
160
        }
161
    }
162
 
163
    /**
164
     * Tests adding a user to the store, assuming that the user has been created using the default 'user default values'.
165
     *
166
     * @covers ::save
167
     */
11 efrain 168
    public function test_save_new_unchanged_user_defaults(): void {
1 efrain 169
        $this->resetAfterTest();
170
        $user = $this->generate_user();
171
        $userrepo = new user_repository();
172
        $sink = $this->redirectEvents();
173
        $saveduser = $userrepo->save($user);
174
        $events = $sink->get_events();
175
        $sink->close();
176
 
177
        $this->assertIsInt($saveduser->get_id());
178
        $this->assert_same_user_values($user, $saveduser, true);
179
        $this->assert_user_db_values($saveduser);
180
        // No change to underlying user: city, etc. take on default values matching those of the existing user record.
181
        $this->assertEmpty($events);
182
    }
183
 
184
    /**
185
     * Tests adding a user to the store, assuming that the user has been created using modified 'user default values'.
186
     *
187
     * @covers ::save
188
     */
11 efrain 189
    public function test_save_new_changed_user_defaults(): void {
1 efrain 190
        $this->resetAfterTest();
191
        $user = $this->generate_user(1, ['city' => 'Perth']);
192
        $userrepo = new user_repository();
193
        $sink = $this->redirectEvents();
194
        $saveduser = $userrepo->save($user);
195
        $events = $sink->get_events();
196
        $sink->close();
197
 
198
        $this->assertIsInt($saveduser->get_id());
199
        $this->assert_same_user_values($user, $saveduser, true);
200
        $this->assert_user_db_values($saveduser);
201
        // The underlying user record will change: city ('Perth') differs from that of the existing user ('').
202
        $this->assertInstanceOf(\core\event\user_updated::class, $events[0]);
203
    }
204
 
205
    /**
206
     * Test saving an existing user instance.
207
     *
208
     * @covers ::save
209
     */
11 efrain 210
    public function test_save_existing(): void {
1 efrain 211
        $this->resetAfterTest();
212
        $user = $this->generate_user();
213
        $userrepo = new user_repository();
214
        $sink = $this->redirectEvents();
215
        $saveduser = $userrepo->save($user);
216
        $events = $sink->get_events();
217
        $sink->close();
218
        $this->assertEmpty($events); // No event for the first save, since the underlying user record is unchanged.
219
 
220
        $saveduser->set_city('New City');
221
        $saveduser->set_country('NZ');
222
        $saveduser->set_lastgrade(99.99999999);
223
        $sink = $this->redirectEvents();
224
        $saveduser2 = $userrepo->save($saveduser);
225
        $events = $sink->get_events();
226
        $sink->close();
227
 
228
        $this->assertEquals($saveduser->get_id(), $saveduser2->get_id());
229
        $this->assert_same_user_values($saveduser, $saveduser2, true);
230
        $this->assert_user_db_values($saveduser2);
231
        // The underlying user record will change now, since city and country have changed.
232
        $this->assertInstanceOf(\core\event\user_updated::class, $events[0]);
233
    }
234
 
235
    /**
236
     * Test saving an instance which exists by id, but has a different localid to the data in the store.
237
     *
238
     * @covers ::save
239
     */
11 efrain 240
    public function test_save_existing_localid_mismatch(): void {
1 efrain 241
        $this->resetAfterTest();
242
        $user = $this->generate_user();
243
        $userrepo = new user_repository();
244
        $saveduser = $userrepo->save($user);
245
 
246
        $user2 = user::create(
247
            $saveduser->get_resourceid(),
248
            999999,
249
            $saveduser->get_deploymentid(),
250
            $saveduser->get_sourceid(),
251
            $saveduser->get_lang(),
252
            $saveduser->get_timezone(),
253
            '',
254
            '',
255
            '',
256
            null,
257
            null,
258
            null,
259
            null,
260
            $saveduser->get_id()
261
        );
262
        $this->expectException(\coding_exception::class);
263
        $this->expectExceptionMessage("Cannot update user mapping. LTI user '{$saveduser->get_id()}' is already mapped " .
264
            "to user '{$saveduser->get_localid()}' and can't be associated with another user '999999'.");
265
        $userrepo->save($user2);
266
    }
267
 
268
    /**
269
     * Test trying to save a user with an id that is invalid.
270
     *
271
     * @covers ::save
272
     */
11 efrain 273
    public function test_save_stale_id(): void {
1 efrain 274
        global $CFG;
275
        $this->resetAfterTest();
276
        $instructoruser = $this->getDataGenerator()->create_user();
277
        $userrepo = new user_repository();
278
        $user = user::create(
279
            4,
280
            $instructoruser->id,
281
            5,
282
            'source-id-123',
283
            $CFG->lang,
284
            '99',
285
            '',
286
            '',
287
            '',
288
            null,
289
            null,
290
            null,
291
            null,
292
            999999
293
        );
294
 
295
        $this->expectException(\coding_exception::class);
296
        $this->expectExceptionMessage("Cannot save lti user with id '999999'. The record does not exist.");
297
        $userrepo->save($user);
298
    }
299
 
300
    /**
301
     * Verify that trying to save a stale object results in an exception referring to unique constraint violation.
302
     *
303
     * @covers ::save
304
     */
11 efrain 305
    public function test_save_uniqueness_constraint(): void {
1 efrain 306
        $this->resetAfterTest();
307
        $user = $this->generate_user();
308
        $userrepo = new user_repository();
309
        $userrepo->save($user);
310
 
311
        $this->expectException(\coding_exception::class);
312
        $this->expectExceptionMessageMatches("/Cannot create duplicate LTI user '[a-z0-9_]*' for resource '[0-9]*'/");
313
        $userrepo->save($user);
314
    }
315
 
316
    /**
317
     * Test finding a user instance by id.
318
     *
319
     * @covers ::find
320
     */
11 efrain 321
    public function test_find(): void {
1 efrain 322
        $this->resetAfterTest();
323
        $user = $this->generate_user();
324
        $userrepo = new user_repository();
325
        $saveduser = $userrepo->save($user);
326
 
327
        $founduser = $userrepo->find($saveduser->get_id());
328
        $this->assertIsInt($founduser->get_id());
329
        $this->assert_same_user_values($saveduser, $founduser, false);
330
 
331
        $this->assertNull($userrepo->find(0));
332
    }
333
 
334
    /**
335
     * Test finding all of users associated with a given published resource.
336
     *
337
     * @covers ::find_by_resource
338
     */
11 efrain 339
    public function test_find_by_resource(): void {
1 efrain 340
        global $CFG;
341
        $this->resetAfterTest();
342
        $user = $this->generate_user();
343
        $userrepo = new user_repository();
344
        $saveduser = $userrepo->save($user);
345
        $instructoruser = $this->getDataGenerator()->create_user();
346
 
347
        $user2 = user::create(
348
            $saveduser->get_resourceid(),
349
            $instructoruser->id,
350
            $saveduser->get_deploymentid(),
351
            'another-user-123',
352
            $CFG->lang,
353
            '99',
354
            'Perth',
355
            'AU',
356
            'An Example Institution',
357
            2
358
        );
359
        $saveduser2 = $userrepo->save($user2);
360
        $savedusers = [$saveduser->get_id() => $saveduser, $saveduser2->get_id() => $saveduser2];
361
 
362
        $foundusers = $userrepo->find_by_resource($saveduser->get_resourceid());
363
        $this->assertCount(2, $foundusers);
364
        foreach ($foundusers as $founduser) {
365
            $this->assert_same_user_values($savedusers[$founduser->get_id()], $founduser);
366
        }
367
    }
368
 
369
    /**
370
     * Test that users can be found based on their resource_link association.
371
     *
372
     * @covers ::find_by_resource_link
373
     */
11 efrain 374
    public function test_find_by_resource_link(): void {
1 efrain 375
        global $CFG;
376
        $this->resetAfterTest();
377
        $user = $this->generate_user();
378
        $user->set_resourcelinkid(33);
379
        $userrepo = new user_repository();
380
        $saveduser = $userrepo->save($user);
381
 
382
        $instructoruser = $this->getDataGenerator()->create_user();
383
        $user2 = user::create(
384
            $saveduser->get_resourceid(),
385
            $instructoruser->id,
386
            $saveduser->get_deploymentid(),
387
            'another-user-123',
388
            $CFG->lang,
389
            '99',
390
            'Perth',
391
            'AU',
392
            'An Example Institution',
393
            2,
394
            null,
395
            null,
396
            33
397
        );
398
        $saveduser2 = $userrepo->save($user2);
399
        $savedusers = [$saveduser->get_id() => $saveduser, $saveduser2->get_id() => $saveduser2];
400
 
401
        $foundusers = $userrepo->find_by_resource_link(33);
402
        $this->assertCount(2, $foundusers);
403
        foreach ($foundusers as $founduser) {
404
            $this->assert_same_user_values($savedusers[$founduser->get_id()], $founduser);
405
        }
406
    }
407
 
408
    /**
409
     * Test checking existence of a user instance, based on id.
410
     *
411
     * @covers ::exists
412
     */
11 efrain 413
    public function test_exists(): void {
1 efrain 414
        $this->resetAfterTest();
415
        $user = $this->generate_user();
416
        $userrepo = new user_repository();
417
        $saveduser = $userrepo->save($user);
418
 
419
        $this->assertTrue($userrepo->exists($saveduser->get_id()));
420
        $this->assertFalse($userrepo->exists(-50));
421
    }
422
 
423
    /**
424
     * Test deleting a user instance, based on id.
425
     *
426
     * @covers ::delete
427
     */
11 efrain 428
    public function test_delete(): void {
1 efrain 429
        $this->resetAfterTest();
430
        $user = $this->generate_user();
431
        $userrepo = new user_repository();
432
        $saveduser = $userrepo->save($user);
433
        $this->assertTrue($userrepo->exists($saveduser->get_id()));
434
 
435
        $userrepo->delete($saveduser->get_id());
436
        $this->assertFalse($userrepo->exists($saveduser->get_id()));
437
 
438
        global $DB;
439
        $this->assertFalse($DB->record_exists('enrol_lti_users', ['id' => $saveduser->get_id()]));
440
        $this->assertFalse($DB->record_exists('enrol_lti_user_resource_link', ['ltiuserid' => $saveduser->get_id()]));
441
        $this->assertTrue($DB->record_exists('user', ['id' => $saveduser->get_localid()]));
442
 
443
        $this->assertNull($userrepo->delete($saveduser->get_id()));
444
    }
445
 
446
    /**
447
     * Test deleting a collection of lti user instances by deployment.
448
     *
449
     * @covers ::delete_by_deployment
450
     */
11 efrain 451
    public function test_delete_by_deployment(): void {
1 efrain 452
        global $CFG;
453
        $this->resetAfterTest();
454
        $user = $this->generate_user();
455
        $userrepo = new user_repository();
456
        $saveduser = $userrepo->save($user);
457
        $instructoruser = $this->getDataGenerator()->create_user();
458
        $instructor2user = $this->getDataGenerator()->create_user();
459
 
460
        $user2 = user::create(
461
            $saveduser->get_resourceid(),
462
            $instructoruser->id,
463
            $saveduser->get_deploymentid(),
464
            'another-user-123',
465
            $CFG->lang,
466
            '99',
467
            'Perth',
468
            'AU',
469
            'An Example Institution',
470
        );
471
        $saveduser2 = $userrepo->save($user2);
472
 
473
        $user3 = user::create(
474
            $saveduser->get_resourceid(),
475
            $instructor2user->id,
476
            $saveduser->get_deploymentid() + 1,
477
            'another-user-678',
478
            $CFG->lang,
479
            '99',
480
            'Melbourne',
481
            'AU',
482
            'An Example Institution',
483
        );
484
        $saveduser3 = $userrepo->save($user3);
485
        $this->assertTrue($userrepo->exists($saveduser->get_id()));
486
        $this->assertTrue($userrepo->exists($saveduser2->get_id()));
487
        $this->assertTrue($userrepo->exists($saveduser3->get_id()));
488
 
489
        $userrepo->delete_by_deployment($saveduser->get_deploymentid());
490
        $this->assertFalse($userrepo->exists($saveduser->get_id()));
491
        $this->assertFalse($userrepo->exists($saveduser2->get_id()));
492
        $this->assertTrue($userrepo->exists($saveduser3->get_id()));
493
    }
494
 
495
    /**
496
     * Verify a user who has been deleted can be re-saved to the repository and matched to an existing local user.
497
     *
498
     * @covers ::save
499
     */
11 efrain 500
    public function test_save_deleted(): void {
1 efrain 501
        $this->resetAfterTest();
502
        $user = $this->generate_user();
503
        $userrepo = new user_repository();
504
        $saveduser = $userrepo->save($user);
505
 
506
        $userrepo->delete($saveduser->get_id());
507
        $this->assertFalse($userrepo->exists($saveduser->get_id()));
508
 
509
        $saveduser2 = $userrepo->save($user);
510
        $this->assertEquals($saveduser->get_localid(), $saveduser2->get_localid());
511
        $this->assertNotEquals($saveduser->get_id(), $saveduser2->get_id());
512
    }
513
 
514
    /**
515
     * Test confirming that any associated legacy lti user records are not returned by the repository.
516
     *
517
     * This test ensures that any enrolment methods (resources) updated in-place from legacy LTI to 1.3 only return LTI 1.3 users.
518
     *
519
     * @covers ::find
520
     * @covers ::find_single_user_by_resource
521
     * @covers ::find_by_resource
522
     */
523
    public function test_find_filters_legacy_lti_users(): void {
524
        $this->resetAfterTest();
525
        global $DB;
526
        $user = $this->getDataGenerator()->create_user();
527
        $course = $this->getDataGenerator()->create_course();
528
        $resource = $this->getDataGenerator()->create_lti_tool((object)['courseid' => $course->id]);
529
        $ltiuserdata = [
530
            'userid' => $user->id,
531
            'toolid' => $resource->id,
532
            'sourceid' => '1001',
533
        ];
534
        $ltiuserid = $DB->insert_record('enrol_lti_users', $ltiuserdata);
535
        $userrepo = new user_repository();
536
 
537
        $this->assertNull($userrepo->find($ltiuserid));
538
        $this->assertNull($userrepo->find_single_user_by_resource($user->id, $resource->id));
539
        $this->assertEmpty($userrepo->find_by_resource($resource->id));
540
 
541
        // Set deploymentid, indicating the user originated from an LTI 1.3 launch and should now be returned.
542
        $ltiuserdata['id'] = $ltiuserid;
543
        $ltiuserdata['ltideploymentid'] = '234';
544
        $DB->update_record('enrol_lti_users', $ltiuserdata);
545
 
546
        $this->assertInstanceOf(user::class, $userrepo->find($ltiuserid));
547
        $this->assertInstanceOf(user::class, $userrepo->find_single_user_by_resource($user->id, $resource->id));
548
        $ltiusers = $userrepo->find_by_resource($resource->id);
549
        $this->assertCount(1, $ltiusers);
550
        $this->assertInstanceOf(user::class, reset($ltiusers));
551
    }
552
}