Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | 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 mod_quiz\local;
18
 
19
use mod_quiz\event\group_override_created;
20
use mod_quiz\event\group_override_updated;
21
use mod_quiz\event\user_override_created;
22
use mod_quiz\event\user_override_updated;
23
use mod_quiz\event\user_override_deleted;
24
use mod_quiz\quiz_settings;
25
 
26
/**
27
 * Test for override_manager class
28
 *
29
 * @package   mod_quiz
30
 * @copyright 2024 Matthew Hilton <matthewhilton@catalyst-au.net>
31
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32
 * @covers    \mod_quiz\local\override_manager
33
 */
34
final class override_manager_test extends \advanced_testcase {
35
    /** @var array Default quiz settings **/
36
    private const TEST_QUIZ_SETTINGS = [
37
        'attempts' => 5,
38
        'timeopen' => 100000000,
39
        'timeclose' => 10000001,
40
        'timelimit' => 10,
41
    ];
42
 
43
    /**
44
     * Create quiz and course for test
45
     *
46
     * @return array containing quiz object and course
47
     */
48
    private function create_quiz_and_course(): array {
49
        $course = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS]);
50
        $quizparams = array_merge(self::TEST_QUIZ_SETTINGS, ['course' => $course->id]);
51
        $quiz = $this->getDataGenerator()->create_module('quiz', $quizparams);
52
        $quizobj = quiz_settings::create($quiz->id);
53
        return [$quizobj, $course];
54
    }
55
 
56
    /**
57
     * Utility function that replaces the placeholders in the given data.
58
     *
59
     * @param array $data
60
     * @param array $placeholdervalues
61
     * @return array the $data with the placeholders replaced
62
     */
63
    private function replace_placeholders(array $data, array $placeholdervalues) {
64
        foreach ($data as $key => $value) {
65
            $replacement = $placeholdervalues[$value] ?? null;
66
 
67
            if (!empty($replacement)) {
68
                $data[$key] = $replacement;
69
            }
70
        }
71
 
72
        return $data;
73
    }
74
 
75
    /**
76
     * Utility function that sets up data for tests testing CRUD operations.
77
     * Placeholders such as ':userid' and ':groupid' can be used in the data to replace with the relevant id.
78
     *
79
     * @param array $existingdata Data used to setup a preexisting quiz override record.
80
     * @param array $formdata submitted formdata
81
     * @return \stdClass containing formdata (after placeholders replaced), quizobj, user and group
82
     */
83
    private function setup_existing_and_testing_data(array $existingdata, array $formdata): \stdClass {
84
        global $DB;
85
 
86
        [$quizobj, $course] = $this->create_quiz_and_course();
87
 
88
        $user = $this->getDataGenerator()->create_user();
89
        $this->getDataGenerator()->enrol_user($user->id, $course->id);
90
 
91
        $user2 = $this->getDataGenerator()->create_user();
92
        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
93
 
94
        $groupid = groups_create_group((object) ['courseid' => $course->id, 'name' => 'test']);
95
        $group2id = groups_create_group((object) ['courseid' => $course->id, 'name' => 'test2']);
96
 
97
        // Replace any userid or groupid placeholders in the form data or existing data.
98
        $placeholdervalues = [
99
            ':userid' => $user->id,
100
            ':user2id' => $user2->id,
101
            ':groupid' => $groupid,
102
            ':group2id' => $group2id,
103
        ];
104
 
105
        if (!empty($existingdata)) {
106
            // Raw insert the existing data for the test into the DB.
107
            // We assume it is valid for the test.
108
            $existingdata['quiz'] = $quizobj->get_quizid();
109
            $existingid = $DB->insert_record('quiz_overrides', $this->replace_placeholders($existingdata, $placeholdervalues));
110
            $placeholdervalues[':existingid'] = $existingid;
111
        }
112
 
113
        $formdata = $this->replace_placeholders($formdata, $placeholdervalues);
114
 
115
        // Add quiz id to formdata.
116
        $formdata['quiz'] = $quizobj->get_quizid();
117
 
118
        return (object) [
119
            'quizobj' => $quizobj,
120
            'formdata' => $formdata,
121
            'user1' => $user,
122
            'groupid1' => $groupid,
123
        ];
124
    }
125
 
126
    /**
127
     * Provides values to test_save_and_get_override
128
     *
129
     * @return array
130
     */
131
    public static function save_and_get_override_provider(): array {
132
        return [
133
            'create user override - no existing data' => [
134
                'existingdata' => [],
135
                'formdata' => [
136
                    'userid' => ':userid',
137
                    'groupid' => null,
138
                    'timeopen' => 50,
139
                    'timeclose' => 51,
140
                    'timelimit' => 1,
141
                    'attempts' => 999,
142
                    'password' => 'test',
143
                ],
144
                'expectedrecordscreated' => 1,
145
                'expectedevent' => user_override_created::class,
146
            ],
147
            'create user override - no calendar events should be created' => [
148
                'existingdata' => [],
149
                'formdata' => [
150
                    'userid' => ':userid',
151
                    'groupid' => null,
152
                    'timeopen' => null,
153
                    'timeclose' => null,
154
                    'timelimit' => 1,
155
                    'attempts' => 999,
156
                    'password' => 'test',
157
                ],
158
                'expectedrecordscreated' => 1,
159
                'expectedevent' => user_override_created::class,
160
            ],
161
            'create user override - only timeopen' => [
162
                'existingdata' => [],
163
                'formdata' => [
164
                    'userid' => ':userid',
165
                    'groupid' => null,
166
                    'timeopen' => 50,
167
                    'timeclose' => null,
168
                    'timelimit' => null,
169
                    'attempts' => null,
170
                    'password' => 'test',
171
                ],
172
                'expectedrecordscreated' => 1,
173
                'expectedevent' => user_override_created::class,
174
            ],
175
            'create group override - no existing data' => [
176
                'existingdata' => [],
177
                'formdata' => [
178
                    'userid' => null,
179
                    'groupid' => ':groupid',
180
                    'timeopen' => 50,
181
                    'timeclose' => 51,
182
                    'timelimit' => 1,
183
                    'attempts' => 999,
184
                    'password' => 'test',
185
                ],
186
                'expectedrecordscreated' => 1,
187
                'expectedevent' => group_override_created::class,
188
            ],
189
            'create group override - no calendar events should be created' => [
190
                'existingdata' => [],
191
                'formdata' => [
192
                    'userid' => null,
193
                    'groupid' => ':groupid',
194
                    'timeopen' => null,
195
                    'timeclose' => null,
196
                    'timelimit' => 1,
197
                    'attempts' => 999,
198
                    'password' => 'test',
199
                ],
200
                'expectedrecordscreated' => 1,
201
                'expectedevent' => group_override_created::class,
202
            ],
203
            'create group override - only timeopen' => [
204
                'existingdata' => [],
205
                'formdata' => [
206
                    'userid' => null,
207
                    'groupid' => ':groupid',
208
                    'timeopen' => 50,
209
                    'timeclose' => null,
210
                    'timelimit' => null,
211
                    'attempts' => null,
212
                    'password' => null,
213
                ],
214
                'expectedrecordscreated' => 1,
215
                'expectedevent' => group_override_created::class,
216
            ],
217
            'update user override - updating existing data' => [
218
                'existingdata' => [
219
                    'userid' => ':userid',
220
                    'groupid' => null,
221
                    'timeopen' => 50,
222
                    'timeclose' => 51,
223
                    'timelimit' => 2,
224
                    'attempts' => 2,
225
                    'password' => 'test2',
226
                ],
227
                'formdata' => [
228
                    'id' => ':existingid',
229
                    'userid' => ':userid',
230
                    'groupid' => null,
231
                    'timeopen' => 52,
232
                    'timeclose' => 53,
233
                    'timelimit' => 1,
234
                    'attempts' => 999,
235
                    'password' => 'test',
236
                ],
237
                'expectedrecordscreated' => 0,
238
                'expectedevent' => user_override_updated::class,
239
            ],
240
            'update group override - updating existing data' => [
241
                'existingdata' => [
242
                    'userid' => null,
243
                    'groupid' => ':groupid',
244
                    'timeopen' => 50,
245
                    'timeclose' => 51,
246
                    'timelimit' => 2,
247
                    'attempts' => 2,
248
                    'password' => 'test2',
249
                ],
250
                'formdata' => [
251
                    'id' => ':existingid',
252
                    'userid' => null,
253
                    'groupid' => ':groupid',
254
                    'timeopen' => 52,
255
                    'timeclose' => 53,
256
                    'timelimit' => 1,
257
                    'attempts' => 999,
258
                    'password' => 'test',
259
                ],
260
                'expectedrecordscreated' => 0,
261
                'expectedevent' => group_override_updated::class,
262
            ],
263
            'attempts is set to unlimited (i.e. 0)' => [
264
                'existingdata' => [],
265
                'formdata' => [
266
                    'userid' => ':userid',
267
                    'groupid' => null,
268
                    'timeopen' => null,
269
                    'timeclose' => null,
270
                    'timelimit' => null,
271
                    // This checks we are using empty() carefully, since this is valid.
272
                    'attempts' => 0,
273
                    'password' => null,
274
                ],
275
                'expectedrecordscreated' => 1,
276
                'expectedevent' => user_override_created::class,
277
            ],
278
            'some settings submitted are the same as what is in the quiz (valid)' => [
279
                'existingdata' => [],
280
                'formdata' => [
281
                    'userid' => ':userid',
282
                    'groupid' => null,
283
                    // Make these the same, they should be ignored.
284
                    'timeopen' => self::TEST_QUIZ_SETTINGS['timeopen'],
285
                    'timeclose' => self::TEST_QUIZ_SETTINGS['timeclose'],
286
                    'attempts' => self::TEST_QUIZ_SETTINGS['attempts'],
287
                    // However change this, this should still get updated.
288
                    'timelimit' => self::TEST_QUIZ_SETTINGS['timelimit'] + 5,
289
                    'password' => null,
290
                ],
291
                'expectedrecordscreated' => 1,
292
                'expectedevent' => user_override_created::class,
293
            ],
294
        ];
295
    }
296
 
297
    /**
298
     * Tests save_override function
299
     *
300
     * @param array $existingdata If given, an existing override will be created.
301
     * @param array $formdata The data being tested, simulating being submitted
302
     * @param int $expectedrecordscreated The number of records that are expected to be created by upsert
303
     * @param string $expectedeventclass an event class, which is expected to the emitted by upsert
304
     * @dataProvider save_and_get_override_provider
305
     */
306
    public function test_save_and_get_override(
307
        array $existingdata,
308
        array $formdata,
309
        int $expectedrecordscreated,
310
        string $expectedeventclass
311
    ): void {
312
        global $DB;
313
 
314
        $this->setAdminUser();
315
        $this->resetAfterTest();
316
 
317
        $test = $this->setup_existing_and_testing_data($existingdata, $formdata);
318
        $manager = $test->quizobj->get_override_manager();
319
 
320
        // Get the count before.
321
        $beforecount = $DB->count_records('quiz_overrides');
322
 
323
        $sink = $this->redirectEvents();
324
 
325
        // Submit the form data.
326
        $id = $manager->save_override($test->formdata);
327
 
328
        // Get the count after and compare to the expected.
329
        $aftercount = $DB->count_records('quiz_overrides');
330
        $this->assertEquals($expectedrecordscreated, $aftercount - $beforecount);
331
 
332
        // Read back the created/updated value, and compare it to the formdata.
333
        $readback = $DB->get_record('quiz_overrides', ['id' => $id]);
334
        $this->assertNotEmpty($readback);
335
 
336
        foreach ($test->formdata as $key => $value) {
337
            // If the value is the same as the quiz, we expect it to be null.
338
            if (!empty(self::TEST_QUIZ_SETTINGS[$key]) && $value == self::TEST_QUIZ_SETTINGS[$key]) {
339
                $this->assertNull($readback->{$key});
340
            } else {
341
                // Else we expect the value to have been set.
342
                $this->assertEquals($value, $readback->{$key});
343
            }
344
        }
345
 
346
        // Check the get_all_overrides function returns this data as well.
347
        $alloverrideids = array_column($manager->get_all_overrides(), 'id');
348
 
349
        $this->assertCount($aftercount, $alloverrideids);
350
        $this->assertTrue(in_array($id, $alloverrideids));
351
 
352
        // Check that the calendar events are created as well.
353
        // This is only if the times were set, and they were set differently to the default.
354
        $expectedcount = 0;
355
 
356
        if (!empty($formdata['timeopen']) && $formdata['timeopen'] != self::TEST_QUIZ_SETTINGS['timeopen']) {
357
            $expectedcount += 1;
358
        }
359
 
360
        if (!empty($formdata['timeclose']) && $formdata['timeclose'] != self::TEST_QUIZ_SETTINGS['timeclose']) {
361
            $expectedcount += 1;
362
        }
363
 
364
        // Find all events. We assume the test event times do not exceed a time of 999.
365
        $events = calendar_get_events(0, 999, [$test->user1->id], [$test->groupid1], false);
366
        $this->assertCount($expectedcount, $events);
367
 
368
        // Check the expected event was also emitted.
369
        if (!empty($expectedeventclass)) {
370
            $events = $sink->get_events();
371
            $eventclasses = array_map(fn($event) => get_class($event), $events);
372
            $this->assertTrue(in_array($expectedeventclass, $eventclasses));
373
        }
374
    }
375
 
376
    /**
377
     * Tests that when saving an override, validation is performed and an exception is thrown if this fails.
378
     * Note - this does not test every validation scenario, for that {@see validate_data_provider}
379
     */
380
    public function test_save_override_validation_throws_exception(): void {
381
        $this->setAdminUser();
382
        $this->resetAfterTest();
383
 
384
        [$quizobj] = $this->create_quiz_and_course();
385
        $manager = $quizobj->get_override_manager();
386
 
387
        // Submit empty (bad data).
388
        $this->expectException(\invalid_parameter_exception::class);
389
        $this->expectExceptionMessage(get_string('nooverridedata', 'quiz'));
390
        $manager->save_override([]);
391
    }
392
 
393
    /**
394
     * Provides values to test_validate_data
395
     *
396
     * @return array
397
     */
398
    public static function validate_data_provider(): array {
399
        return [
400
            'valid create for user' => [
401
                'existingdata' => [],
402
                'formdata' => [
403
                    'userid' => ':userid',
404
                    'groupid' => null,
405
                    'timeopen' => 50,
406
                    'timeclose' => 51,
407
                    'timelimit' => 2,
408
                    'attempts' => 2,
409
                    'password' => 'test2',
410
                ],
411
                'expectedreturn' => [],
412
            ],
413
            'valid create for group' => [
414
                'existingdata' => [],
415
                'formdata' => [
416
                    'userid' => null,
417
                    'groupid' => ':groupid',
418
                    'timeopen' => 50,
419
                    'timeclose' => 51,
420
                    'timelimit' => 2,
421
                    'attempts' => 2,
422
                    'password' => 'test2',
423
                ],
424
                'expectedreturn' => [],
425
            ],
426
            'valid update for user' => [
427
                'existingdata' => [
428
                    'userid' => ':userid',
429
                    'groupid' => null,
430
                    'timeopen' => 50,
431
                    'timeclose' => 51,
432
                    'timelimit' => 2,
433
                    'attempts' => 2,
434
                    'password' => 'test2',
435
                ],
436
                'formdata' => [
437
                    'id' => ':existingid',
438
                    'userid' => ':userid',
439
                    'groupid' => null,
440
                    'timeopen' => 50,
441
                    'timeclose' => 51,
442
                    'timelimit' => 2,
443
                    'attempts' => 2,
444
                    'password' => 'test2',
445
                ],
446
                'expectedreturn' => [],
447
            ],
448
            'valid update for group' => [
449
                'existingdata' => [
450
                    'userid' => null,
451
                    'groupid' => ':groupid',
452
                    'timeopen' => 50,
453
                    'timeclose' => 51,
454
                    'timelimit' => 2,
455
                    'attempts' => 2,
456
                    'password' => 'test2',
457
                ],
458
                'formdata' => [
459
                    'id' => ':existingid',
460
                    'userid' => null,
461
                    'groupid' => ':groupid',
462
                    'timeopen' => 50,
463
                    'timeclose' => 51,
464
                    'timelimit' => 2,
465
                    'attempts' => 2,
466
                    'password' => 'test2',
467
                ],
468
                'expectedreturn' => [],
469
            ],
470
            'update but without user or group specified in update' => [
471
                'existingdata' => [
472
                    'userid' => ':userid',
473
                    'groupid' => null,
474
                    'timeopen' => 50,
475
                    'timeclose' => 51,
476
                    'timelimit' => 2,
477
                    'attempts' => 2,
478
                    'password' => 'test2',
479
                ],
480
                'formdata' => [
481
                    'id' => ':existingid',
482
                    'userid' => null,
483
                    'groupid' => null,
484
                    'timeopen' => 52,
485
                    'timeclose' => 53,
486
                    'timelimit' => 1,
487
                    'attempts' => 999,
488
                    'password' => 'test',
489
                ],
490
                'expectedreturn' => [
491
                    'general' => get_string('overridemustsetuserorgroup', 'quiz'),
492
                ],
493
            ],
494
            'both userid and groupid specified' => [
495
                'existingdata' => [],
496
                'formdata' => [
497
                    'userid' => ':userid',
498
                    'groupid' => ':groupid',
499
                    'timeopen' => 50,
500
                    'timeclose' => 100,
501
                    'timelimit' => null,
502
                    'attempts' => null,
503
                    'password' => null,
504
                ],
505
                'expectedreturn' => [
506
                    'general' => get_string('overridecannotsetbothgroupanduser', 'quiz'),
507
                ],
508
            ],
509
            'neither userid nor groupid specified' => [
510
                'existingdata' => [],
511
                'formdata' => [
512
                    'userid' => null,
513
                    'groupid' => null,
514
                    'timeopen' => 50,
515
                    'timeclose' => 100,
516
                    'timelimit' => null,
517
                    'attempts' => null,
518
                    'password' => null,
519
                ],
520
                'expectedreturn' => [
521
                    'general' => get_string('overridemustsetuserorgroup', 'quiz'),
522
                ],
523
            ],
524
            'empty data' => [
525
                'existingdata' => [],
526
                'formdata' => [],
527
                'expectedreturn' => [
528
                    'general' => get_string('nooverridedata', 'quiz'),
529
                ],
530
            ],
531
            'all nulls' => [
532
                'existingdata' => [],
533
                'formdata' => [
534
                    'userid' => null,
535
                    'groupid' => null,
536
                    'timeopen' => null,
537
                    'timeclose' => null,
538
                    'timelimit' => null,
539
                    'attempts' => null,
540
                    'password' => null,
541
                ],
542
                'expectedreturn' => [
543
                    'general' => get_string('nooverridedata', 'quiz'),
544
                ],
545
            ],
546
            'user given, rest are nulls' => [
547
                'existingdata' => [],
548
                'formdata' => [
549
                    'userid' => ':userid',
550
                    'groupid' => null,
551
                    'timeopen' => null,
552
                    'timeclose' => null,
553
                    'timelimit' => null,
554
                    'attempts' => null,
555
                    'password' => null,
556
                ],
557
                'expectedreturn' => [
558
                    'general' => get_string('nooverridedata', 'quiz'),
559
                ],
560
            ],
561
            'all submitted data was the same as the existing quiz' => [
562
                'existingdata' => [],
563
                'formdata' => [
564
                    'userid' => ':userid',
565
                    'groupid' => null,
566
                    'timeopen' => self::TEST_QUIZ_SETTINGS['timeopen'],
567
                    'timeclose' => self::TEST_QUIZ_SETTINGS['timeclose'],
568
                    'attempts' => self::TEST_QUIZ_SETTINGS['attempts'],
569
                    'timelimit' => self::TEST_QUIZ_SETTINGS['timelimit'],
570
                    'password' => null,
571
                ],
572
                'expectedreturn' => [
573
                    'general' => get_string('nooverridedata', 'quiz'),
574
                ],
575
            ],
576
            'userid is invalid' => [
577
                'existingdata' => [],
578
                'formdata' => [
579
                    'userid' => -1,
580
                    'groupid' => null,
581
                    'timeopen' => null,
582
                    'timeclose' => null,
583
                    'timelimit' => null,
584
                    'attempts' => null,
585
                    'password' => 'mypass',
586
                ],
587
                'expectedreturn' => [
588
                    'userid' => get_string('overrideinvaliduser', 'quiz'),
589
                ],
590
            ],
591
            'groupid is invalid' => [
592
                'existingdata' => [],
593
                'formdata' => [
594
                    'userid' => null,
595
                    'groupid' => -1,
596
                    'timeopen' => null,
597
                    'timeclose' => null,
598
                    'timelimit' => null,
599
                    'attempts' => null,
600
                    'password' => 'mypass',
601
                ],
602
                'expectedreturn' => [
603
                    'groupid' => get_string('overrideinvalidgroup', 'quiz'),
604
                ],
605
            ],
606
            'timeclose is before timeopen' => [
607
                'existingdata' => [],
608
                'formdata' => [
609
                    'userid' => ':userid',
610
                    'groupid' => null,
611
                    'timeopen' => 50,
612
                    'timeclose' => 10,
613
                    'timelimit' => null,
614
                    'attempts' => null,
615
                    'password' => null,
616
                ],
617
                'expectedreturn' => [
618
                    'timeclose' => get_string('closebeforeopen', 'quiz'),
619
                ],
620
            ],
621
            'timeclose is same as timeopen' => [
622
                'existingdata' => [],
623
                'formdata' => [
624
                    'userid' => ':userid',
625
                    'groupid' => null,
626
                    'timeopen' => 50,
627
                    'timeclose' => 50,
628
                    'timelimit' => null,
629
                    'attempts' => null,
630
                    'password' => null,
631
                ],
632
                'expectedreturn' => [
633
                    'timeclose' => get_string('closebeforeopen', 'quiz'),
634
                ],
635
            ],
636
            'timelimit is negative' => [
637
                'existingdata' => [],
638
                'formdata' => [
639
                    'userid' => ':userid',
640
                    'groupid' => null,
641
                    'timeopen' => null,
642
                    'timeclose' => null,
643
                    'timelimit' => -1,
644
                    'attempts' => null,
645
                    'password' => null,
646
                ],
647
                'expectedreturn' => [
648
                    'timelimit' => get_string('overrideinvalidtimelimit', 'quiz'),
649
                ],
650
            ],
651
            'attempts is negative' => [
652
                'existingdata' => [],
653
                'formdata' => [
654
                    'userid' => ':userid',
655
                    'groupid' => null,
656
                    'timeopen' => null,
657
                    'timeclose' => null,
658
                    'timelimit' => null,
659
                    'attempts' => -1,
660
                    'password' => null,
661
                ],
662
                'expectedreturn' => [
663
                    'attempts' => get_string('overrideinvalidattempts', 'quiz'),
664
                ],
665
            ],
666
            'existing id given to update is invalid' => [
667
                'existingdata' => [],
668
                'formdata' => [
669
                    'id' => 999999999999,
670
                    'userid' => ':userid',
671
                    'groupid' => null,
672
                    'timeopen' => 50,
673
                    'timeclose' => 51,
674
                    'timelimit' => null,
675
                    'attempts' => null,
676
                    'password' => null,
677
                ],
678
                'expectedreturn' => [
679
                    'general' => get_string('overrideinvalidexistingid', 'quiz'),
680
                ],
681
            ],
682
            'userid changed after creation' => [
683
                'existingdata' => [
684
                    'userid' => ":userid",
685
                    'groupid' => null,
686
                    'timeopen' => 50,
687
                    'timeclose' => 51,
688
                    'timelimit' => 2,
689
                    'attempts' => 2,
690
                    'password' => 'test2',
691
                ],
692
                'formdata' => [
693
                    'id' => ':existingid',
694
                    'userid' => ":user2id",
695
                    'groupid' => null,
696
                    'timeopen' => 50,
697
                    'timeclose' => 51,
698
                    'timelimit' => null,
699
                    'attempts' => null,
700
                    'password' => null,
701
                ],
702
                'expectedreturn' => [
703
                   'userid' => get_string('overridecannotchange', 'quiz'),
704
                ],
705
            ],
706
            'groupid changed after creation' => [
707
                'existingdata' => [
708
                    'userid' => null,
709
                    'groupid' => ':groupid',
710
                    'timeopen' => 50,
711
                    'timeclose' => 51,
712
                    'timelimit' => 2,
713
                    'attempts' => 2,
714
                    'password' => 'test2',
715
                ],
716
                'formdata' => [
717
                    'id' => ':existingid',
718
                    'userid' => null,
719
                    'groupid' => ':group2id',
720
                    'timeopen' => 50,
721
                    'timeclose' => 51,
722
                    'timelimit' => null,
723
                    'attempts' => null,
724
                    'password' => null,
725
                ],
726
                'expectedreturn' => [
727
                   'groupid' => get_string('overridecannotchange', 'quiz'),
728
                ],
729
            ],
730
            'create multiple for the same user' => [
731
                'existingdata' => [
732
                    'userid' => ':userid',
733
                    'groupid' => null,
734
                    'timeopen' => 50,
735
                    'timeclose' => 51,
736
                    'timelimit' => 2,
737
                    'attempts' => 2,
738
                    'password' => 'test2',
739
                ],
740
                'formdata' => [
741
                    'userid' => ':userid',
742
                    'groupid' => null,
743
                    'timeopen' => 50,
744
                    'timeclose' => 51,
745
                    'timelimit' => 2,
746
                    'attempts' => 2,
747
                    'password' => 'test2',
748
                ],
749
                'expectedreturn' => [
750
                    'general' => get_string('overridemultiplerecordsexist', 'quiz'),
751
                ],
752
            ],
753
            'create multiple for the same group' => [
754
                'existingdata' => [
755
                    'userid' => null,
756
                    'groupid' => ':groupid',
757
                    'timeopen' => 50,
758
                    'timeclose' => 51,
759
                    'timelimit' => 2,
760
                    'attempts' => 2,
761
                    'password' => 'test2',
762
                ],
763
                'formdata' => [
764
                    'userid' => null,
765
                    'groupid' => ':groupid',
766
                    'timeopen' => 50,
767
                    'timeclose' => 51,
768
                    'timelimit' => 2,
769
                    'attempts' => 2,
770
                    'password' => 'test2',
771
                ],
772
                'expectedreturn' => [
773
                    'general' => get_string('overridemultiplerecordsexist', 'quiz'),
774
                ],
775
            ],
776
        ];
777
    }
778
 
779
    /**
780
     * Tests validate_data function
781
     *
782
     * @param array $existingdata If given, an existing override will be created.
783
     * @param array $formdata The data being tested, simulating being submitted
784
     * @param array $expectedreturn expected keys and associated values expected to be returned from validate_data
785
     * @dataProvider validate_data_provider
786
     */
787
    public function test_validate_data(array $existingdata, array $formdata, array $expectedreturn): void {
788
        $this->setAdminUser();
789
        $this->resetAfterTest();
790
 
791
        // Setup the test.
792
        $test = $this->setup_existing_and_testing_data($existingdata, $formdata);
793
 
794
        // Validate.
795
        $manager = $test->quizobj->get_override_manager();
796
        $result = $manager->validate_data($test->formdata);
797
 
798
        // Ensure all expected errors appear in the return.
799
        foreach ($expectedreturn as $key => $error) {
800
            // Ensure it is set.
801
            $this->assertContains($key, array_keys($result));
802
 
803
            // Ensure the message contains the expected error.
804
            $this->assertStringContainsString($error, $result[$key]);
805
        }
806
 
807
        // Ensure there are no extra returned errors than what was expected.
808
        $extra = array_diff_key($result, $expectedreturn);
809
        $this->assertEmpty($extra, 'More validation errors were returned than expected');
810
    }
811
 
812
    /**
813
     * Provide delete functions to test
814
     *
815
     * @return array
816
     */
817
    public static function delete_override_provider(): array {
818
        return [
819
            'delete by id (no events logged)' => [
820
                'function' => fn($manager, $override) => $manager->delete_overrides_by_id([$override->id], false, false),
821
                'checkeventslogged' => false,
822
            ],
823
            'delete single (no events logged)' => [
824
                'function' => fn($manager, $override) => $manager->delete_overrides([$override], false, false),
825
                'checkeventslogged' => false,
826
            ],
827
            'delete all (no events logged)' => [
828
                'function' => fn($manager, $override) => $manager->delete_all_overrides(false, false),
829
                'checkeventslogged' => false,
830
            ],
831
            'delete by id (events logged)' => [
832
                'function' => fn($manager, $override) => $manager->delete_overrides_by_id([$override->id], true, false),
833
                'checkeventslogged' => true,
834
            ],
835
            'delete single (events logged)' => [
836
                'function' => fn($manager, $override) => $manager->delete_overrides([$override], true, false),
837
                'checkeventslogged' => true,
838
            ],
839
            'delete all (events logged)' => [
840
                'function' => fn($manager, $override) => $manager->delete_all_overrides(true, false),
841
                'checkeventslogged' => true,
842
            ],
843
            'delete all in database (events logged)' => [
844
                'function' => fn($manager, $override) => $manager->delete_all_overrides(true, false),
845
                'checkeventslogged' => true,
846
            ],
847
        ];
848
    }
849
 
850
    /**
851
     * Tests deleting override functions
852
     *
853
     * @param \Closure $deletefunction delete function to be called.
854
     * @param bool $checkeventslogged if true, will check that events were logged.
855
     * @dataProvider delete_override_provider
856
     */
857
    public function test_delete_override(\Closure $deletefunction, bool $checkeventslogged): void {
858
        $this->setAdminUser();
859
        $this->resetAfterTest();
860
        [$quizobj] = $this->create_quiz_and_course();
861
        $user = $this->getDataGenerator()->create_user();
862
 
863
        // Create an override.
864
        $data = [
865
            'userid' => $user->id,
866
            'timeopen' => 500,
867
        ];
868
        $manager = $quizobj->get_override_manager();
869
        $id = $manager->save_override($data);
870
 
871
        // Check the calendar event was made.
872
        $this->assertCount(1, calendar_get_events(0, 999, [$user->id], false, false));
873
 
874
        // Check that the cache was made.
875
        $overridecache = new override_cache($quizobj->get_quizid());
876
        $this->assertNotEmpty($overridecache->get_cached_user_override($user->id));
877
 
878
        // Capture events.
879
        $sink = $this->redirectEvents();
880
 
881
        $override = (object) [
882
            'id' => $id,
883
            'userid' => $user->id,
884
        ];
885
 
886
        // Delete the override.
887
        $deletefunction($manager, $override);
888
 
889
        // Check the calendar event was deleted.
890
        $this->assertCount(0, calendar_get_events(0, 999, [$user->id], false, false));
891
 
892
        // Check that the cache was cleared.
893
        $this->assertEmpty($overridecache->get_cached_user_override($user->id));
894
 
895
        // Check the event was logged.
896
        if ($checkeventslogged) {
897
            $events = $sink->get_events();
898
            $eventclasses = array_map(fn($e) => get_class($e), $events);
899
            $this->assertTrue(in_array(user_override_deleted::class, $eventclasses));
900
        }
901
    }
902
 
903
    /**
904
     * Creates a role with the given capabilities and assigns it to the user.
905
     *
906
     * @param int $userid user to assign role to
907
     * @param array $capabilities array of $capname => $permission to add to role
908
     */
909
    private function give_user_role_with_capabilities(int $userid, array $capabilities): void {
910
        // Setup the role and permissions.
911
        $roleid = $this->getDataGenerator()->create_role();
912
        foreach ($capabilities as $capname => $permission) {
913
            role_change_permission($roleid, \context_system::instance(), $capname, $permission);
914
        }
915
 
916
        $user = $this->getDataGenerator()->create_user();
917
        role_assign($roleid, $userid, \context_system::instance()->id);
918
    }
919
 
920
    /**
921
     * Provides values to test_require_read_capability
922
     *
923
     * @return array
924
     */
925
    public static function require_read_capability_provider(): array {
926
        $readfunc = fn($manager) => $manager->require_read_capability();
927
        $managefunc = fn($manager) => $manager->require_manage_capability();
928
 
929
        return [
930
            'reading - cannot read' => [
931
                'capabilitiestogive' => [],
932
                'expectedallowed' => false,
933
                'functionbeingtested' => $readfunc,
934
            ],
935
            'reading - can read' => [
936
                'capabilitiestogive' => ['mod/quiz:viewoverrides' => CAP_ALLOW],
937
                'expectedallowed' => true,
938
                'functionbeingtested' => $readfunc,
939
            ],
940
            'reading - can manage (so can also read)' => [
941
                'capabilitiestogive' => ['mod/quiz:manageoverrides' => CAP_ALLOW],
942
                'expectedallowed' => true,
943
                'functionbeingtested' => $readfunc,
944
            ],
945
            'manage - cannot manage' => [
946
                'capabilitiestogive' => [],
947
                'expectedallowed' => false,
948
                'functionbeingtested' => $managefunc,
949
            ],
950
            'manage - can only read' => [
951
                'capabilitiestogive' => ['mod/quiz:viewoverrides' => CAP_ALLOW],
952
                'expectedallowed' => false,
953
                'functionbeingtested' => $managefunc,
954
            ],
955
            'manage - can manage' => [
956
                'capabilitiestogive' => ['mod/quiz:manageoverrides' => CAP_ALLOW],
957
                'expectedallowed' => true,
958
                'functionbeingtested' => $managefunc,
959
            ],
960
        ];
961
    }
962
 
963
    /**
964
     * Tests require_read_capability
965
     *
966
     * @param array $capabilitiestogive array of capability => value to give to test user
967
     * @param bool $expectedallowed if false, will expect required_capability_exception to be thrown
968
     * @param \Closure $functionbeingtested is passed the manager and calls the function being tested (usually require_*_capability)
969
     * @dataProvider require_read_capability_provider
970
     */
971
    public function test_require_read_capability(
972
        array $capabilitiestogive,
973
        bool $expectedallowed,
974
        \Closure $functionbeingtested
975
    ): void {
976
        $this->resetAfterTest();
977
        [$quizobj] = $this->create_quiz_and_course();
978
        $user = $this->getDataGenerator()->create_user();
979
        $this->give_user_role_with_capabilities($user->id, $capabilitiestogive);
980
        $this->setUser($user);
981
 
982
        if (!$expectedallowed) {
983
            $this->expectException(\required_capability_exception::class);
984
        }
985
        $functionbeingtested($quizobj->get_override_manager());
986
    }
987
 
988
    /**
989
     * Tests delete_orphaned_group_overrides_in_course
990
     */
991
    public function test_delete_orphaned_group_overrides_in_course(): void {
992
        global $DB;
993
 
994
        $this->setAdminUser();
995
        $this->resetAfterTest();
996
        [$quizobj, $course] = $this->create_quiz_and_course();
997
 
998
        // Create a two group and one user overrides.
999
        $groupid = groups_create_group((object) ['courseid' => $course->id, 'name' => 'test']);
1000
        $groupdata = [
1001
            'quiz' => $quizobj->get_quizid(),
1002
            'groupid' => $groupid,
1003
            'password' => 'test',
1004
        ];
1005
 
1006
        $group2id = groups_create_group((object) ['courseid' => $course->id, 'name' => 'test2']);
1007
        $group2data = [
1008
            'quiz' => $quizobj->get_quizid(),
1009
            'groupid' => $group2id,
1010
            'password' => 'test',
1011
        ];
1012
 
1013
        $userid = $this->getDataGenerator()->create_user()->id;
1014
        $userdata = [
1015
            'quiz' => $quizobj->get_quizid(),
1016
            'userid' => $userid,
1017
            'password' => 'test',
1018
        ];
1019
 
1020
        $manager = $quizobj->get_override_manager();
1021
        $manager->save_override($groupdata);
1022
        $useroverrideid = $manager->save_override($userdata);
1023
        $group2overrideid = $manager->save_override($group2data);
1024
 
1025
        $this->assertCount(3, $manager->get_all_overrides());
1026
 
1027
        // Delete the first group (via the DB, so that the callbacks are not run).
1028
        $DB->delete_records('groups', ['id' => $groupid]);
1029
 
1030
        // Confirm the overrides still exist (no callback has been run yet).
1031
        $this->assertCount(3, $manager->get_all_overrides());
1032
 
1033
        // Run orphaned record callback.
1034
        override_manager::delete_orphaned_group_overrides_in_course($course->id);
1035
 
1036
        // Confirm it has now been deleted (but user and other group override still exists).
1037
        $overrides = $manager->get_all_overrides();
1038
        $this->assertCount(2, $overrides);
1039
        $this->assertArrayHasKey($useroverrideid, $overrides);
1040
        $this->assertArrayHasKey($group2overrideid, $overrides);
1041
    }
1042
 
1043
    /**
1044
     * Tests deleting by id but providing an invalid id
1045
     */
1046
    public function test_delete_by_id_invalid_id(): void {
1047
        $this->setAdminUser();
1048
        $this->resetAfterTest();
1049
        [$quizobj] = $this->create_quiz_and_course();
1050
 
1051
        $this->expectException(\invalid_parameter_exception::class);
1052
        $this->expectExceptionMessage(get_string('overridemissingdelete', 'quiz', '0,1'));
1053
 
1054
        // These ids do not exist, so this should throw an error.
1055
        $quizobj->get_override_manager()->delete_overrides_by_id([0, 1]);
1056
    }
1057
 
1058
    /**
1059
     * Tests that constructing a override manager with mismatching quiz and context throws an exception
1060
     */
1061
    public function test_quiz_context_mismatch(): void {
1062
        $this->resetAfterTest();
1063
 
1064
        // Create one quiz for context, but make the quiz given have an incorrect cmid.
1065
        [$quizobj] = $this->create_quiz_and_course();
1066
        $context = \context_module::instance($quizobj->get_cmid());
1067
 
1068
        $quiz = (object)[
1069
            'cmid' => $context->instanceid + 1,
1070
        ];
1071
 
1072
        $this->expectException(\coding_exception::class);
1073
        $this->expectExceptionMessage("Given context does not match the quiz object");
1074
        new override_manager($quiz, $context);
1075
    }
1076
}