1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Test for extensions manager.
19 *
20 * @package    core_contentbank
21 * @category   test
22 * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26namespace core_contentbank;
27
28defined('MOODLE_INTERNAL') || die();
29
30use advanced_testcase;
31use context_course;
32use context_coursecat;
33use context_system;
34
35global $CFG;
36require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
37require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
38
39/**
40 * Test for extensions manager.
41 *
42 * @package    core_contentbank
43 * @category   test
44 * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
45 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46 * @coversDefaultClass \core_contentbank\contentbank
47 */
48class core_contentbank_testcase extends advanced_testcase {
49
50    /**
51     * Setup to ensure that fixtures are loaded.
52     */
53    public static function setupBeforeClass(): void {
54        global $CFG;
55
56        require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
57    }
58
59    /**
60     * Data provider for test_get_extension_supporter.
61     *
62     * @return  array
63     */
64    public function get_extension_provider() {
65        return [
66            'H5P file' => ['something.h5p', '.h5p'],
67            'PDF file' => ['something.pdf', '.pdf']
68        ];
69    }
70
71    /**
72     * Tests for get_extension() function.
73     *
74     * @dataProvider    get_extension_provider
75     * @param   string  $filename    The filename given
76     * @param   string   $expected   The extension of the file
77     *
78     * @covers ::get_extension
79     */
80    public function test_get_extension(string $filename, string $expected) {
81        $this->resetAfterTest();
82
83        $cb = new contentbank();
84
85        $extension = $cb->get_extension($filename);
86        $this->assertEquals($expected, $extension);
87    }
88
89    /**
90     * Data provider for test_load_context_supported_extensions.
91     *
92     * @return  array
93     */
94    public function get_extension_supporters_provider() {
95        return [
96            'H5P first' => [['.h5p' => ['h5p', 'testable']], '.h5p', 'h5p'],
97            'Testable first (but upload not implemented)' => [['.h5p' => ['testable', 'h5p']], '.h5p', 'h5p'],
98        ];
99    }
100
101    /**
102     * Tests for get_extension_supporter() function with admin permissions.
103     *
104     * @dataProvider    get_extension_supporters_provider
105     * @param   array   $supporters   The content type plugin supporters for each extension
106     * @param   string  $extension    The extension of the file given
107     * @param   string  $expected   The supporter contenttype of the file
108     *
109     * @covers ::load_context_supported_extensions
110     */
111    public function test_get_extension_supporter_for_admins(array $supporters, string $extension, string $expected) {
112        $this->resetAfterTest();
113
114        $cb = new contentbank();
115
116        $systemcontext = context_system::instance();
117
118        // All contexts allowed for admins.
119        $this->setAdminUser();
120        $contextsupporters = $cb->load_context_supported_extensions($systemcontext);
121        $this->assertArrayHasKey($extension, $contextsupporters);
122        $this->assertEquals($expected, $contextsupporters[$extension]);
123    }
124
125    /**
126     * Tests for get_extension_supporter() function with user default permissions.
127     *
128     * @dataProvider    get_extension_supporters_provider
129     * @param   array   $supporters   The content type plugin supporters for each extension
130     * @param   string  $extension    The extension of the file given
131     * @param   string  $expected   The supporter contenttype of the file
132     *
133     * @covers ::load_context_supported_extensions
134     */
135    public function test_get_extension_supporter_for_users(array $supporters, string $extension, string $expected) {
136        $this->resetAfterTest();
137
138        $cb = new contentbank();
139        $systemcontext = context_system::instance();
140
141        // Set a user with no permissions.
142        $user = $this->getDataGenerator()->create_user();
143        $this->setUser($user);
144
145        // Users with no capabilities can't upload content.
146        $contextsupporters = $cb->load_context_supported_extensions($systemcontext);
147        $this->assertEquals([], $contextsupporters);
148    }
149
150    /**
151     * Tests for get_extension_supporter() function with teacher defaul permissions.
152     *
153     * @dataProvider    get_extension_supporters_provider
154     * @param   array   $supporters   The content type plugin supporters for each extension
155     * @param   string  $extension    The extension of the file given
156     * @param   string  $expected   The supporter contenttype of the file
157     *
158     * @covers ::load_context_supported_extensions
159     */
160    public function test_get_extension_supporter_for_teachers(array $supporters, string $extension, string $expected) {
161        $this->resetAfterTest();
162
163        $cb = new contentbank();
164
165        $course = $this->getDataGenerator()->create_course();
166        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
167        $this->setUser($teacher);
168        $coursecontext = context_course::instance($course->id);
169
170        // Teachers has permission in their context to upload supported by H5P content type.
171        $contextsupporters = $cb->load_context_supported_extensions($coursecontext);
172        $this->assertArrayHasKey($extension, $contextsupporters);
173        $this->assertEquals($expected, $contextsupporters[$extension]);
174    }
175
176    /**
177     * Tests for get_extension_supporter() function.
178     *
179     * @dataProvider    get_extension_supporters_provider
180     * @param   array   $supporters   The content type plugin supporters for each extension
181     * @param   string  $extension    The extension of the file given
182     * @param   string  $expected   The supporter contenttype of the file
183     *
184     * @covers ::get_extension_supporter
185     */
186    public function test_get_extension_supporter(array $supporters, string $extension, string $expected) {
187        $this->resetAfterTest();
188
189        $cb = new contentbank();
190        $systemcontext = context_system::instance();
191        $this->setAdminUser();
192
193        $supporter = $cb->get_extension_supporter($extension, $systemcontext);
194        $this->assertEquals($expected, $supporter);
195    }
196
197    /**
198     * Test the behaviour of search_contents().
199     *
200     * @dataProvider search_contents_provider
201     * @param  string $search String to search.
202     * @param  string $where Context where to search.
203     * @param  int $expectedresult Expected result.
204     * @param  array $contexts List of contexts where to create content.
205     */
206    public function test_search_contents(?string $search, string $where, int $expectedresult, array $contexts = [],
207            array $contenttypes = null): void {
208        global $DB;
209
210        $this->resetAfterTest();
211
212        // Create users.
213        $managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']);
214        $manager = $this->getDataGenerator()->create_user();
215        $this->getDataGenerator()->role_assign($managerroleid, $manager->id);
216
217        // Create a category and a course.
218        $coursecat = $this->getDataGenerator()->create_category();
219        $course = $this->getDataGenerator()->create_course();
220        $existingcontexts = [];
221        $existingcontexts['system'] = \context_system::instance();
222        $existingcontexts['category'] = \context_coursecat::instance($coursecat->id);
223        $existingcontexts['course'] = \context_course::instance($course->id);
224
225        if (empty($where)) {
226            $contextid = 0;
227        } else {
228            $contextid = $existingcontexts[$where]->id;
229        }
230
231        // Add some content to the content bank.
232        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
233        foreach ($contexts as $context) {
234            $contextinstance = $existingcontexts[$context];
235            $records = $generator->generate_contentbank_data('contenttype_h5p', 3,
236                $manager->id, $contextinstance, false);
237        }
238
239        // Search for some content.
240        $cb = new contentbank();
241        $contents = $cb->search_contents($search, $contextid, $contenttypes);
242
243        $this->assertCount($expectedresult, $contents);
244        if (!empty($contents) && !empty($search)) {
245            foreach ($contents as $content) {
246                $this->assertContains($search, $content->get_name());
247            }
248        }
249    }
250
251    /**
252     * Data provider for test_search_contents().
253     *
254     * @return array
255     */
256    public function search_contents_provider(): array {
257
258        return [
259            'Search all content in all contexts' => [
260                null,
261                '',
262                9,
263                ['system', 'category', 'course']
264            ],
265            'Search in all contexts for existing string in all contents' => [
266                'content',
267                '',
268                9,
269                ['system', 'category', 'course']
270            ],
271            'Search in all contexts for unexisting string in all contents' => [
272                'chocolate',
273                '',
274                0,
275                ['system', 'category', 'course']
276            ],
277            'Search in all contexts for existing string in some contents' => [
278                '1',
279                '',
280                3,
281                ['system', 'category', 'course']
282            ],
283            'Search in all contexts for existing string in some contents (create only 1 context)' => [
284                '1',
285                '',
286                1,
287                ['system']
288            ],
289            'Search in system context for existing string in all contents' => [
290                'content',
291                'system',
292                3,
293                ['system', 'category', 'course']
294            ],
295            'Search in category context for unexisting string in all contents' => [
296                'chocolate',
297                'category',
298                0,
299                ['system', 'category', 'course']
300            ],
301            'Search in course context for existing string in some contents' => [
302                '1',
303                'course',
304                1,
305                ['system', 'category', 'course']
306            ],
307            'Search in system context' => [
308                null,
309                'system',
310                3,
311                ['system', 'category', 'course']
312            ],
313            'Search in course context with existing content' => [
314                null,
315                'course',
316                3,
317                ['system', 'category', 'course']
318            ],
319            'Search in course context without existing content' => [
320                null,
321                'course',
322                0,
323                ['system', 'category']
324            ],
325            'Search in an empty contentbank' => [
326                null,
327                '',
328                0,
329                []
330            ],
331            'Search in a context in an empty contentbank' => [
332                null,
333                'system',
334                0,
335                []
336            ],
337            'Search for a string in an empty contentbank' => [
338                'content',
339                '',
340                0,
341                []
342            ],
343            'Search with unexisting content-type' => [
344                null,
345                'course',
346                0,
347                ['system', 'category', 'course'],
348                ['contenttype_unexisting'],
349            ],
350        ];
351    }
352
353    /**
354     * Test create_content_from_file function.
355     *
356     * @covers ::create_content_from_file
357     */
358    public function test_create_content_from_file() {
359        global $USER;
360
361        $this->resetAfterTest();
362        $this->setAdminUser();
363        $systemcontext = \context_system::instance();
364        $name = 'dummy_h5p.h5p';
365
366        // Create a dummy H5P file.
367        $dummyh5p = array(
368            'contextid' => $systemcontext->id,
369            'component' => 'contentbank',
370            'filearea' => 'public',
371            'itemid' => 1,
372            'filepath' => '/',
373            'filename' => $name,
374            'userid' => $USER->id
375        );
376        $fs = get_file_storage();
377        $dummyh5pfile = $fs->create_file_from_string($dummyh5p, 'Dummy H5Pcontent');
378
379        $cb = new contentbank();
380        $content = $cb->create_content_from_file($systemcontext, $USER->id, $dummyh5pfile);
381
382        $this->assertEquals('contenttype_h5p', $content->get_content_type());
383        $this->assertInstanceOf('\\contenttype_h5p\\content', $content);
384        $this->assertEquals($name, $content->get_name());
385    }
386
387    /**
388     * Test the behaviour of delete_contents().
389     *
390     * @covers  ::delete_contents
391     */
392    public function test_delete_contents() {
393        global $DB;
394
395        $this->resetAfterTest();
396        $cb = new \core_contentbank\contentbank();
397
398        // Create a category and two courses.
399        $systemcontext = context_system::instance();
400        $coursecat = $this->getDataGenerator()->create_category();
401        $coursecatcontext = context_coursecat::instance($coursecat->id);
402        $course1 = $this->getDataGenerator()->create_course();
403        $course1context = context_course::instance($course1->id);
404        $course2 = $this->getDataGenerator()->create_course();
405        $course2context = context_course::instance($course2->id);
406
407        // Add some content to the content bank.
408        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
409        $systemcontent = $generator->generate_contentbank_data(null, 3, 0, $systemcontext);
410        $categorycontent = $generator->generate_contentbank_data(null, 3, 0, $coursecatcontext);
411        $course1content = $generator->generate_contentbank_data(null, 3, 0, $course1context);
412        $course2content = $generator->generate_contentbank_data(null, 3, 0, $course2context);
413
414        // Check the content has been created as expected.
415        $this->assertEquals(12, $DB->count_records('contentbank_content'));
416
417        // Check the system content is deleted as expected and the rest of the content is not.
418        $this->assertTrue($cb->delete_contents($systemcontext));
419        $this->assertEquals(0, $DB->count_records('contentbank_content', ['contextid' => $systemcontext->id]));
420        // And the rest of the context content exists.
421        $this->assertEquals(9, $DB->count_records('contentbank_content'));
422
423        // Check the course category content is deleted as expected and the rest of the content is not.
424        $this->assertTrue($cb->delete_contents($coursecatcontext));
425        $this->assertEquals(0, $DB->count_records('contentbank_content', ['contextid' => $coursecatcontext->id]));
426        // And the rest of the context content exists.
427        $this->assertEquals(6, $DB->count_records('contentbank_content'));
428
429        // Check the course content is deleted as expected and the rest of the content is not.
430        $this->assertTrue($cb->delete_contents($course1context));
431        $this->assertEquals(0, $DB->count_records('contentbank_content', ['contextid' => $course1context->id]));
432        // And the rest of the context content exists.
433        $this->assertEquals(3, $DB->count_records('contentbank_content'));
434    }
435
436    /**
437     * Test the behaviour of delete_contents() for empty content bank.
438     *
439     * @covers  ::delete_contents
440     */
441    public function test_delete_contents_for_empty_contentbank() {
442
443        $this->resetAfterTest();
444        $cb = new \core_contentbank\contentbank();
445
446        // Create a category and two courses.
447        $systemcontext = \context_system::instance();
448        $coursecat = $this->getDataGenerator()->create_category();
449        $coursecatcontext = \context_coursecat::instance($coursecat->id);
450        $course = $this->getDataGenerator()->create_course();
451        $coursecontext = \context_course::instance($course->id);
452
453        // Check there's no error when trying to delete content from an empty content bank.
454        $this->assertTrue($cb->delete_contents($systemcontext));
455        $this->assertTrue($cb->delete_contents($coursecatcontext));
456        $this->assertTrue($cb->delete_contents($coursecontext));
457    }
458
459    /**
460     * Test the behaviour of move_contents().
461     *
462     * @covers  ::move_contents
463     */
464    public function test_move_contents() {
465        global $DB;
466
467        $this->resetAfterTest();
468        $cb = new \core_contentbank\contentbank();
469
470        // Create a category and two courses.
471        $course1 = $this->getDataGenerator()->create_course();
472        $course1context = context_course::instance($course1->id);
473        $course2 = $this->getDataGenerator()->create_course();
474        $course2context = context_course::instance($course2->id);
475
476        // Add some content to the content bank.
477        $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
478        $course1content = $generator->generate_contentbank_data(null, 3, 0, $course1context);
479        $course2content = $generator->generate_contentbank_data(null, 3, 0, $course2context);
480
481        // Check the content has been created as expected.
482        $this->assertEquals(6, $DB->count_records('contentbank_content'));
483        $this->assertEquals(3, $DB->count_records('contentbank_content', ['contextid' => $course1context->id]));
484
485        // Check the content is moved to another context as expected and the rest of the content is not.
486        $this->assertTrue($cb->move_contents($course1context, $course2context));
487        $this->assertEquals(6, $DB->count_records('contentbank_content'));
488        $this->assertEquals(0, $DB->count_records('contentbank_content', ['contextid' => $course1context->id]));
489        $this->assertEquals(6, $DB->count_records('contentbank_content', ['contextid' => $course2context->id]));
490    }
491
492    /**
493     * Test the behaviour of move_contents() for empty content bank.
494     *
495     * @covers  ::move_contents
496     */
497    public function test_move_contents_for_empty_contentbank() {
498
499        $this->resetAfterTest();
500        $cb = new \core_contentbank\contentbank();
501
502        // Create a category and two courses.
503        $systemcontext = \context_system::instance();
504        $course = $this->getDataGenerator()->create_course();
505        $coursecontext = \context_course::instance($course->id);
506
507        // Check there's no error when trying to move content context from an empty content bank.
508        $this->assertTrue($cb->delete_contents($systemcontext, $coursecontext));
509    }
510
511    /**
512     * Data provider for get_contenttypes_with_capability_feature.
513     *
514     * @return  array
515     */
516    public function get_contenttypes_with_capability_feature_provider(): array {
517        return [
518            'no-contenttypes_enabled' => [
519                'contenttypesenabled' => [],
520                'contenttypescanfeature' => [],
521            ],
522            'contenttype_enabled_noeditable' => [
523                'contenttypesenabled' => ['testable'],
524                'contenttypescanfeature' => [],
525            ],
526            'contenttype_enabled_editable' => [
527                'contenttypesenabled' => ['testable'],
528                'contenttypescanfeature' => ['testable'],
529            ],
530            'no-contenttype_enabled_editable' => [
531                'contenttypesenabled' => [],
532                'contenttypescanfeature' => ['testable'],
533            ],
534        ];
535    }
536
537    /**
538     * Tests for get_contenttypes_with_capability_feature() function.
539     *
540     * @dataProvider    get_contenttypes_with_capability_feature_provider
541     * @param   array $contenttypesenabled Content types enabled.
542     * @param   array $contenttypescanfeature Content types the user has the permission to use the feature.
543     *
544     * @covers ::get_contenttypes_with_capability_feature
545     */
546    public function test_get_contenttypes_with_capability_feature(array $contenttypesenabled, array $contenttypescanfeature): void {
547        $this->resetAfterTest();
548
549        $cb = new contentbank();
550
551        $plugins = [];
552
553        // Content types not enabled where the user has permission to use a feature.
554        if (empty($contenttypesenabled) && !empty($contenttypescanfeature)) {
555            $enabled = false;
556
557            // Mock core_plugin_manager class and the method get_plugins_of_type.
558            $pluginmanager = $this->getMockBuilder(\core_plugin_manager::class)
559                ->disableOriginalConstructor()
560                ->setMethods(['get_plugins_of_type'])
561                ->getMock();
562
563            // Replace protected singletoninstance reference (core_plugin_manager property) with mock object.
564            $ref = new \ReflectionProperty(\core_plugin_manager::class, 'singletoninstance');
565            $ref->setAccessible(true);
566            $ref->setValue(null, $pluginmanager);
567
568            // Return values of get_plugins_of_type method.
569            foreach ($contenttypescanfeature as $contenttypepluginname) {
570                $contenttypeplugin = new \stdClass();
571                $contenttypeplugin->name = $contenttypepluginname;
572                $contenttypeplugin->type = 'contenttype';
573                // Add the feature to the fake content type.
574                $classname = "\\contenttype_$contenttypepluginname\\contenttype";
575                $classname::$featurestotest = ['test2'];
576                $plugins[] = $contenttypeplugin;
577            }
578
579            // Set expectations and return values.
580            $pluginmanager->expects($this->once())
581                ->method('get_plugins_of_type')
582                ->with('contenttype')
583                ->willReturn($plugins);
584        } else {
585            $enabled = true;
586            // Get access to private property enabledcontenttypes.
587            $rc = new \ReflectionClass(\core_contentbank\contentbank::class);
588            $rcp = $rc->getProperty('enabledcontenttypes');
589            $rcp->setAccessible(true);
590
591            foreach ($contenttypesenabled as $contenttypename) {
592                $plugins["\\contenttype_$contenttypename\\contenttype"] = $contenttypename;
593                // Add to the testable contenttype the feature to test.
594                if (in_array($contenttypename, $contenttypescanfeature)) {
595                    $classname = "\\contenttype_$contenttypename\\contenttype";
596                    $classname::$featurestotest = ['test2'];
597                }
598            }
599            // Set as enabled content types only those in the test.
600            $rcp->setValue($cb, $plugins);
601        }
602
603        $actual = $cb->get_contenttypes_with_capability_feature('test2', null, $enabled);
604        $this->assertEquals($contenttypescanfeature, array_values($actual));
605    }
606
607    /**
608     * Test the behaviour of is_context_allowed().
609     *
610     * @dataProvider context_provider
611     * @param  \Closure $getcontext Get the context to check.
612     * @param  bool $expectedresult Expected result.
613     *
614     * @covers ::is_context_allowed
615     */
616    public function test_is_context_allowed(\Closure $getcontext, bool $expectedresult): void {
617        $this->resetAfterTest();
618
619        $cb = new contentbank();
620        $context = $getcontext();
621        $this->assertEquals($expectedresult, $cb->is_context_allowed($context));
622    }
623
624    /**
625     * Data provider for test_is_context_allowed().
626     *
627     * @return array
628     */
629    public function context_provider(): array {
630
631        return [
632            'System context' => [
633                function (): \context {
634                    return \context_system::instance();
635                },
636                true,
637            ],
638            'User context' => [
639                function (): \context {
640                    $user = $this->getDataGenerator()->create_user();
641                    return \context_user::instance($user->id);
642                },
643                false,
644            ],
645            'Course category context' => [
646                function (): \context {
647                    $coursecat = $this->getDataGenerator()->create_category();
648                    return \context_coursecat::instance($coursecat->id);
649                },
650                true,
651            ],
652            'Course context' => [
653                function (): \context {
654                    $course = $this->getDataGenerator()->create_course();
655                    return \context_course::instance($course->id);
656                },
657                true,
658            ],
659            'Module context' => [
660                function (): \context {
661                    $course = $this->getDataGenerator()->create_course();
662                    $module = $this->getDataGenerator()->create_module('page', ['course' => $course->id]);
663                    return \context_module::instance($module->cmid);
664                },
665                false,
666            ],
667            'Block context' => [
668                function (): \context {
669                    $course = $this->getDataGenerator()->create_course();
670                    $coursecontext = context_course::instance($course->id);
671                    $block = $this->getDataGenerator()->create_block('online_users', ['parentcontextid' => $coursecontext->id]);
672                    return \context_block::instance($block->id);
673                },
674                false,
675            ],
676        ];
677    }
678}
679