ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç external/view_page_test.php000064400000005452152265340310012113 0ustar00. namespace core_my\external; use externallib_advanced_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Test Class for external function core_my_view_page. * * @package core_my * @category external * @copyright 2023 Rodrigo Mady * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.3 * @covers \core_my\external\view_page */ final class view_page_test extends externallib_advanced_testcase { /** * Set up for every test. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Helper. * * @param string $page * @return array */ protected function view_page(string $page): array { $result = view_page::execute($page); return \core_external\external_api::clean_returnvalue(view_page::execute_returns(), $result); } /** * Test for webservice my view page. */ public function test_view_page(): void { $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Trigger and capture the event. $sink = $this->redirectEvents(); // Request to trigger the view event in my. $result = $this->view_page('my'); $this->assertTrue($result['status']); $this->assertEmpty($result['warnings']); // Request to trigger the view event in dashboard. $result = $this->view_page('dashboard'); $this->assertTrue($result['status']); $this->assertEmpty($result['warnings']); // Wrong page to trigger the event. $result = $this->view_page('test'); $this->assertFalse($result['status']); $this->assertNotEmpty($result['warnings']); $events = $sink->get_events(); // Check if the log still with two rows. $this->assertCount(2, $events); $this->assertInstanceOf('\core\event\mycourses_viewed', $events[0]); $this->assertInstanceOf('\core\event\dashboard_viewed', $events[1]); $sink->close(); } } privacy/provider_test.php000064400000003453152265340310011631 0ustar00. /** * Unit tests for ltiservice_gradebookservices privacy provider. * * @package ltiservice_gradebookservices * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace ltiservice_gradebookservices\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\tests\provider_testcase; /** * Unit tests for ltiservice_gradebookservices privacy provider. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid(): void { $user = $this->getDataGenerator()->create_user(); $contextlist = \ltiservice_gradebookservices\privacy\provider::get_contexts_for_userid($user->id); $this->assertEmpty($contextlist); } } event/events_test.php000064400000006764152265340310010757 0ustar00. /** * Events tests. * * @package booktool_print * @category phpunit * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace booktool_print\event; /** * Events tests class. * * @package booktool_print * @category phpunit * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class events_test extends \advanced_testcase { public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } public function test_book_printed(): void { // There is no proper API to call to test the event, so what we are // doing here is simply making sure that the events returns the right information. $course = $this->getDataGenerator()->create_course(); $book = $this->getDataGenerator()->create_module('book', array('course' => $course->id)); $context = \context_module::instance($book->cmid); $event = \booktool_print\event\book_printed::create_from_book($book, $context); // Triggering and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\booktool_print\event\book_printed', $event); $this->assertEquals(\context_module::instance($book->cmid), $event->get_context()); $this->assertEquals($book->id, $event->objectid); } public function test_chapter_printed(): void { // There is no proper API to call to test the event, so what we are // doing here is simply making sure that the events returns the right information. $course = $this->getDataGenerator()->create_course(); $book = $this->getDataGenerator()->create_module('book', array('course' => $course->id)); $bookgenerator = $this->getDataGenerator()->get_plugin_generator('mod_book'); $chapter = $bookgenerator->create_chapter(array('bookid' => $book->id)); $context = \context_module::instance($book->cmid); $event = \booktool_print\event\chapter_printed::create_from_chapter($book, $context, $chapter); // Triggering and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\booktool_print\event\chapter_printed', $event); $this->assertEquals(\context_module::instance($book->cmid), $event->get_context()); $this->assertEquals($chapter->id, $event->objectid); } } behat/my_courses.feature000064400000032535152265340310011405 0ustar00@core @core_my @block_myoverview Feature: Run tests over my courses page Background: Given the following "users" exist: | username | firstname | lastname | email | | user1 | User | 1 | user1@example.com | And the following "categories" exist: | name | category | idnumber | | CatA | 0 | cata | And the following "roles" exist: | shortname | name | archetype | | role1 | Role 1 | | And the following "system role assigns" exist: | user | role | contextlevel | reference | | user1 | role1 | Category | CatA | Scenario: Admin can add new courses or manage them from my courses Given I am on the "My courses" page logged in as "admin" And "Create course" "button" should not exist in the "page-header" "region" And "Manage courses" "button" should not exist in the "page-header" "region" When I click on "Create course" "button" in the "page-content" "region" Then I should see "Add a new course" And I am on the "My courses" page And I click on "Manage course categories" "button" in the "page-content" "region" And I should see "Manage course categories and courses" # Check that the expected buttons are displayed in the header when the user is enrolled in a course. But the following "course" exists: | fullname | Course 1 | | shortname | C1 | | format | topics | And the following "course enrolment" exists: | user | admin | | course | C1 | | role | student | And I am on the "My courses" page And "Create course" "button" should exist in the "page-header" "region" And "Manage courses" "button" should exist in the "page-header" "region" And "Create course" "button" should not exist in the "page-content" "region" And "Manage courses" "button" should not exist in the "page-content" "region" And "Manage course categories" "button" should not exist in the "page-content" "region" Scenario: User without creating a course and managing category permissions cannot see any link When I am on the "My courses" page logged in as "user1" Then "Create course" "button" should not exist And "Manage courses" "button" should not exist And "Manage course categories" "button" should not exist # Check that the same buttons are displayed in the header when the user is enrolled in a course. But the following "course" exists: | fullname | Course 1 | | shortname | C1 | | format | topics | And the following "course enrolment" exists: | user | user1 | | course | C1 | | role | student | And I am on the "My courses" page And "Create course" "button" should not exist And "Manage courses" "button" should not exist And "Manage course categories" "button" should not exist Scenario: User without capability to browse courses cannot see any link Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/category:viewcourselist | Prevent | user | System | | When I am on the "My courses" page logged in as "user1" Then "Create course" "button" should not exist And "Manage courses" "button" should not exist And "Manage course categories" "button" should not exist # Check that the same buttons are displayed in the header when the user is enrolled in a course. But the following "course" exists: | fullname | Course 1 | | shortname | C1 | | format | topics | And the following "course enrolment" exists: | user | user1 | | course | C1 | | role | student | And I am on the "My courses" page And "Create course" "button" should not exist And "Manage courses" "button" should not exist And "Manage course categories" "button" should not exist @javascript Scenario: User with creating a course permission can see the Create course link only Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/course:create | Allow | role1 | Category | cata | When I am on the "My courses" page logged in as "user1" Then "Create course" "button" should exist in the "page-content" "region" But "Manage course categories" "button" should not exist And "Create course" "button" should not exist in the "page-header" "region" And I click on "Create course" "button" And I should see "Add a new course" And "CatA" "autocomplete_selection" should exist # Check that the same buttons are displayed in the header when the user is enrolled in a course. But the following "course" exists: | fullname | Course 1 | | shortname | C1 | | format | topics | And the following "course enrolment" exists: | user | user1 | | course | C1 | | role | student | And I am on the "My courses" page And "Create course" "button" should exist in the "page-header" "region" And "Manage courses" "button" should not exist And "Create course" "button" should not exist in the "page-content" "region" Scenario: User with managing a category permission can see the Manage course link only Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/category:manage | Allow | role1 | Category | cata | When I am on the "My courses" page logged in as "user1" Then "Manage course categories" "button" should exist in the "page-content" "region" And "Create course" "button" should not exist And I click on "Manage course categories" "button" in the "page-content" "region" And I should see "Manage course categories and courses" # Check that the same buttons are displayed in the header when the user is enrolled in a course. But the following "course" exists: | fullname | Course 1 | | shortname | C1 | | format | topics | And the following "course enrolment" exists: | user | user1 | | course | C1 | | role | student | And I am on the "My courses" page And "Manage courses" "button" should exist in the "page-header" "region" And "Create course" "button" should not exist And "Manage courses" "button" should not exist in the "page-content" "region" @javascript Scenario: User with both creating a course and managing a category permission can see both links Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/course:create | Allow | role1 | Category | cata | | moodle/category:manage | Allow | role1 | Category | cata | When I am on the "My courses" page logged in as "user1" Then "Create course" "button" should exist in the "page-content" "region" And "Manage course categories" "button" should exist in the "page-content" "region" And "Create course" "button" should not exist in the "page-header" "region" And "Manage courses" "button" should not exist in the "page-header" "region" And I click on "Create course" "button" And I should see "Add a new course" And "CatA" "autocomplete_selection" should exist And I am on the "My courses" page And I click on "Manage course categories" "button" And I should see "Manage course categories and courses" # Check that the same buttons are displayed in the header when the user is enrolled in a course. But the following "course" exists: | fullname | Course 1 | | shortname | C1 | | format | topics | And the following "course enrolment" exists: | user | user1 | | course | C1 | | role | student | And I am on the "My courses" page And "Create course" "button" should exist in the "page-header" "region" And "Manage courses" "button" should exist in the "page-header" "region" And "Create course" "button" should not exist in the "page-content" "region" And "Manage courses" "button" should not exist in the "page-content" "region" @javascript Scenario: Admin can see relevant blocks but not add or move them Given I log in as "admin" And I am on site homepage And I turn editing mode on And I add the "Text" block to the default region with: | Text block title | Text on all pages | | Content | This is visible on all pages | And I configure the "Text on all pages" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | | Default region | Right | And I click on "Save changes" "button" in the "Configure Text on all pages block" "dialogue" And I should see "This is visible on all pages" And "Move Text on all pages block" "menuitem" should exist in the "Text on all pages" "block" When I am on the "My courses" page # Check blocks visible but are "locked" in place. Then "Course overview" "text" should exist in the "region-main" "region" And I should not see "Add a block" And I should see "This is visible on all pages" And "Move Text on all pages block" "menuitem" should not exist in the "Text on all pages" "block" And "Move Course overview block" "menuitem" should not exist in the "Course overview" "block" And "Actions menu" "icon" in the "Course overview" "block" should not be visible Scenario: User with creating a course permission can't see the Request course link Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/course:request | Allow | user | System | | When I am on the "My courses" page logged in as "admin" Then "Create course" "button" should exist in the "page-content" "region" And "Request a course" "button" should not exist And "Create course" "button" should not exist in the "page-header" "region" # Check that the same buttons are displayed in the header when the user is enrolled in a course. But the following "course" exists: | fullname | Course 1 | | shortname | C1 | | format | topics | And the following "course enrolment" exists: | user | admin | | course | C1 | | role | student | And I am on the "My courses" page And "Create course" "button" should exist in the "page-header" "region" And "Request a course" "button" should not exist And "Create course" "button" should not exist in the "page-content" "region" Scenario: User without creating a course but with course request permission could see the Request course link Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/course:request | Allow | user | System | | When I am on the "My courses" page logged in as "user1" Then "Request a course" "button" should exist in the "page-content" "region" And "Create course" "button" should not exist in the "page-content" "region" And "Create course" "button" should not exist in the "page-header" "region" And "Request a course" "button" should not exist in the "page-header" "region" # Check the request a course button is not displayed when this feature is disabled. And the following config values are set as admin: | enablecourserequests | 0 | And I am on the "My courses" page logged in as "user1" And "Request a course" "button" should not exist # Check that the same buttons are displayed in the header when the user is enrolled in a course. But the following "course" exists: | fullname | Course 1 | | shortname | C1 | | format | topics | And the following "course enrolment" exists: | user | user1 | | course | C1 | | role | student | And the following config values are set as admin: | enablecourserequests | 1 | And I am on the "My courses" page And "Request a course" "button" should exist in the "page-header" "region" And "Create course" "button" should not exist And "Request a course" "button" should not exist in the "page-content" "region" Scenario: User without creating nor course request permission shouldn't see any Request course link Given I am on the "My courses" page logged in as "user1" Then "Request a course" "button" should not exist in the "page-content" "region" And "Create course" "button" should not exist in the "page-content" "region" And "Manage courses" "button" should not exist in the "page-content" "region" # Check that the same buttons are displayed in the header when the user is enrolled in a course. But the following "course" exists: | fullname | Course 1 | | shortname | C1 | | format | topics | And the following "course enrolment" exists: | user | user1 | | course | C1 | | role | student | And I am on the "My courses" page And "Create course" "button" should not exist And "Request a course" "button" should not exist And "Manage courses" "button" should not exist behat/reset_all_pages.feature000064400000007666152265340310012355 0ustar00@core @core_my Feature: Reset all personalised pages to default In order to reset everyone's personalised pages As an admin I need to press a button on the pages to customise the default pages Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | And the following "role capability" exists: | role | user | | moodle/block:edit | allow | | block/myprofile:addinstance | allow | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | comments | User | student1 | my-index | side-pre | | myprofile | User | student2 | user-profile | side-pre | And I log in as "student1" And I should see "Comments" And I log out And I log in as "student2" And I follow "Profile" in the user menu And I should see "Logged in user" And I log out And I log in as "student3" And I should not see "Comments" And I follow "Profile" in the user menu And I should not see "Logged in user" And I log out Scenario: Reset Dashboard for all users Given I log in as "admin" And I navigate to "Appearance > Default Dashboard page" in site administration And I turn editing mode on And I add the "Latest announcements" block And I open the "Timeline" blocks action menu And I follow "Delete Timeline block" And I press "Yes" And I turn editing mode off And I log out And I log in as "student1" And I should not see "Latest announcements" And I should see "Timeline" And I log out And I log in as "student3" And I should not see "Latest announcements" And I should see "Timeline" And I log out And I log in as "admin" And I navigate to "Appearance > Default Dashboard page" in site administration When I press "Reset Dashboard for all users" And I should see "All Dashboard pages have been reset to default." And I log out And I log in as "student1" Then I should see "Latest announcements" And I should not see "Comments" And I should not see "Timeline" And I log out And I log in as "student3" And I should see "Latest announcements" And I should not see "Timeline" And I log out # Check that this did not affect the customised profiles. And I am on the "student2" "user > profile" page logged in as student2 And I should see "Logged in user" And I should not see "Latest announcements" Scenario: Reset profile for all users Given I log in as "admin" And I navigate to "Appearance > Default profile page" in site administration And I turn editing mode on And I add the "Latest announcements" block And I log out And I am on the "student2" "user > profile" page logged in as student2 And I should not see "Latest announcements" And I log out And I am on the "student3" "user > profile" page logged in as student3 And I should not see "Latest announcements" And I log out And I log in as "admin" And I navigate to "Appearance > Default profile page" in site administration When I press "Reset profile for all users" And I should see "All profile pages have been reset to default." And I log out And I am on the "student2" "user > profile" page logged in as student2 Then I should see "Latest announcements" And I should not see "Logged in user" And I log out And I am on the "student3" "user > profile" page logged in as student3 And I should see "Latest announcements" And I log out # Check that this did not affect the customised dashboards. And I log in as "student1" And I should see "Comments" And I should not see "Latest announcements" behat/preferences_navigation.feature000064400000011711152265340310013726 0ustar00@core @core_my @javascript Feature: Navigate and use preferences page In order to navigate through preferences page As a user I need to be able to use preferences page Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Sam | Student | s1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And I log in as "admin" Scenario Outline: Navigating through user menu Preferences When I follow "Preferences" in the user menu # Click each link in the 'Preferences' page. And I click on "" "link" in the "#page-content" "css_element" # Confirm that each redirected page has 'Preferences' in the breadcrumbs. And "Users" "link" should not exist in the ".breadcrumb" "css_element" Then "Preferences" "link" should exist in the ".breadcrumb" "css_element" # Additional confirmation that breadcrumbs is correct. And "" "text" should exist in the ".breadcrumb" "css_element" # Confirm that user name and profile picture are displayed in header section. And I should see "Admin User" in the ".page-header-headings" "css_element" And ".page-header-image" "css_element" should exist in the "#page-header" "css_element" Examples: | userprefpage | | Edit profile | | Change password | | Preferred language | | Forum preferences | | Editor preferences | | Calendar preferences | | Content bank preferences | | Message preferences | | Notification preferences | | Manage badges | | Badge preferences | | Backpack settings | | This user's role assignments | | Permissions | | Check permissions | | Blog preferences | | External blogs | | Register an external blog | Scenario Outline: Navigating through course participant preferences Given I am on "Course 1" course homepage And I navigate to course participants And I follow "Sam Student" When I click on "Preferences" "link" in the "#region-main-box" "css_element" Then I should see "Sam Student" in the ".page-header-headings" "css_element" And ".page-header-image" "css_element" should exist in the "#page-header" "css_element" # Click each link in the 'Preferences' page. And I click on "" "link" in the "#page-content" "css_element" # Confirm that each redirected page has 'Users/{user}/Preferences' in the breadcrumbs. Then "Users" "link" should exist in the ".breadcrumb" "css_element" And "Sam Student" "link" should exist in the ".breadcrumb" "css_element" And "Preferences" "link" should exist in the ".breadcrumb" "css_element" # Additional confirmation that breadcrumbs is correct. And "" "text" should exist in the ".breadcrumb" "css_element" # Confirm that user name and profile picture are displayed in header section. And I should see "Sam Student" in the ".page-header-headings" "css_element" And ".page-header-image" "css_element" should exist in the "#page-header" "css_element" Examples: | courseprefpage | | Edit profile | | Preferred language | | Forum preferences | | Editor preferences | | Calendar preferences | | Content bank preferences | | Message preferences | | Notification preferences | | This user's role assignments | | Permissions | | Check permissions | Scenario: Navigation with Event monitoring enabled Given I navigate to "Reports > Event monitoring rules" in site administration And I click on "Enable" "link" And I press "Add a new rule" And I set the following fields to these values: | Rule name | Testing1 | | Area to monitor | Subsystem (core) | | Event | Allow role override | And I press "Save changes" When I follow "Preferences" in the user menu # Confirm that Event monitoring is visible and clickable. Then I should see "Miscellaneous" And I follow "Event monitoring" # Confirm that user can subscribe to new rule. And "Subscribe to rule \"Testing1\"" "link" should exist And I am on "Course 1" course homepage And I navigate to course participants And I follow "Sam Student" And I click on "Preferences" "link" in the "#region-main-box" "css_element" # Confirm that admin cannot change student's event monitor subscription. And I should not see "Event monitoring" behat/add_blocks.feature000064400000001546152265340310011300 0ustar00@core @core_user Feature: Add blocks to my profile page In order to add more functionality to my profile page As a user I need to add blocks to my profile page Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | And I log in as "admin" And I follow "Profile" in the user menu Scenario: Add blocks to page When I turn editing mode on And I add the "Latest announcements" block Then I should see "Latest announcements" behat/restrict_available_blocks.feature000064400000003163152265340310014404 0ustar00@core @core_my Feature: Restrict which blocks can be added to Dashboard In order to restrict which blocks can be added As a student I need to ensure I can add the blocks As an admin I need to remove the capability to add a blocks As a student I need to ensure I can't add the blocks any more Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | Scenario: The comments block can be added to Dashboard by default And I log in as "student1" And I turn editing mode on Then the add block selector should contain "Comments" block And the add block selector should contain "Text" block And the add block selector should contain "Tags" block Scenario: Remove the ability to add the comments block to Dashboard Given the following "role capability" exists: | role | user | | block/comments:myaddinstance | prohibit | | block/course_list:myaddinstance | prohibit | | block/html:myaddinstance | prohibit | When I log in as "student1" And I turn editing mode on Then the add block selector should not contain "Comments" block And the add block selector should not contain "Courses" block And the add block selector should not contain "Text" block And the add block selector should contain "Tags" block behat/welcome.feature000064400000003645152265340310010650 0ustar00@core @core_my Feature: Welcome message In order to welcome new or existing user As a user I will see welcome message when I log into moodle Scenario: Log in and being redirected to course page Given the following "users" exist: | username | password | firstname | lastname | email | | wf | test | Fei | Wang | fei@example.com | And the following "courses" exist: | fullname | shortname | | Math 101 | M1O1 | When I am on "Math 101" course homepage And I should see "You are not logged in" in the "page-footer" "region" And I set the field "Username" to "wf" And I set the field "Password" to "test" And I press "Log in" And I should see "Math 101" in the "page-header" "region" And I should not see "Welcome, Fei!" in the "page-header" "region" And I follow "Dashboard" Then I should see "Welcome, Fei!" in the "page-header" "region" @javascript Scenario: Log in and being redirected to default home page When I log in as "admin" And I should see "You are logged in as Admin User" in the "page-footer" "region" And I should see "Welcome, Admin!" in the "page-header" "region" And I log out And I should see "You are not logged in" in the "page-footer" "region" And I log in as "admin" Then I should see "Hi, Admin!" in the "page-header" "region" @accessibility @javascript Scenario Outline: The start page must meet accessibility standards when the welcome message is displayed Given the following config values are set as admin: | defaulthomepage | | When I log in as "admin" Then I should see "Welcome, Admin!" in the "page-header" "region" And the page should meet accessibility standards Examples: | defaulthomepage | # Home. | 0 | # Dashboard. | 1 | # My courses. | 2 | behat/reset_page.feature000064400000001630152265340310011323 0ustar00@core @core_user Feature: Reset my profile page to default In order to remove customisations from my profile page As a user I need to reset my profile page Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | And I log in as "admin" And I follow "Profile" in the user menu Scenario: Add blocks to page and reset When I turn editing mode on And I add the "Latest announcements" block And I press "Reset page to default" Then I should not see "Latest announcements" fixtures/mock_sitepolicy_handler.php000064400000005437152265473400014041 0ustar00. /** * Mock handler for site policies * * @package core_privacy * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Mock handler for site policies * * @package core_privacy * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mock_sitepolicy_handler extends \core_privacy\local\sitepolicy\handler { /** * Returns URL to redirect user to when user needs to agree to site policy * * This is a regular interactive page for web users. It should have normal Moodle header/footers, it should * allow user to view policies and accept them. * * @param bool $forguests * @return moodle_url|null (returns null if site policy is not defined) */ public static function get_redirect_url($forguests = false) { return 'http://example.com/policy.php'; } /** * Returns URL of the site policy that needs to be displayed to the user (inside iframe or to use in WS such as mobile app) * * This page should not have any header/footer, it does not also have any buttons/checkboxes. The caller needs to implement * the "Accept" button and call {@link self::accept()} on completion. * * @param bool $forguests * @return moodle_url|null */ public static function get_embed_url($forguests = false) { return 'http://example.com/view.htm'; } /** * Accept site policy for the current user * * @return bool - false if sitepolicy not defined, user is not logged in or user has already agreed to site policy; * true - if we have successfully marked the user as agreed to the site policy */ public static function accept() { global $USER, $DB; // Accepts policy on behalf of the current user. We set it to 2 here to check that this callback was called. $USER->policyagreed = 2; if (!isguestuser()) { $DB->update_record('user', ['policyagreed' => 2, 'id' => $USER->id]); } return true; } } fixtures/mock_mod_with_user_data_provider.php000064400000003536152265473400015731 0ustar00. /** * Test provider using a fake plugin name. * * @package core_privacy * @copyright 2018 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_testcomponent4\privacy; use core_privacy\local\request\writer; /** * Mock core_user_data_provider for unit tests. * @copyright 2018 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, \core_privacy\local\request\user_preference_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'notimplemented'; } /** * Export all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { writer::export_user_preference('mod_testcomponent4', 'mykey', 'myvalue', 'mydescription'); } } fixtures/mock_plugin_subplugin_provider.php000064400000003375152265473400015457 0ustar00. /** * Test provider using a fake plugin name. * * @package core_privacy * @copyright 2018 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_testcomponent3\privacy; use core_privacy\local\metadata\collection; /** * Mock shared_data_provider for unit tests. * @copyright 2018 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\subplugin_provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection = new collection('testcomponent3'); $collection->add_database_table('testtable', ['testfield1', 'testfield2'], 'testsummary'); return $collection; } } fixtures/provider_throwing_exception.php000064400000006250152265473400014772 0ustar00. /** * Test provider which has issues. * * @package core_privacy * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_component_broken\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\contextlist; use core_privacy\local\request\approved_contextlist; /** * Mock core_user_data_provider for unit tests. * * @package core_privacy * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider { /** * Get metadata. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { throw new \coding_exception(__FUNCTION__); } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist { throw new \coding_exception(__FUNCTION__); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { throw new \coding_exception(__FUNCTION__); } /** * Delete all data for all users in the specified context. * * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { throw new \coding_exception(__FUNCTION__); } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { throw new \coding_exception(__FUNCTION__); } } fixtures/provider_a.php000064400000006212152265473400011271 0ustar00. /** * Test provider which works. * * @package core_privacy * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_component_a\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\metadata\collection; use core_privacy\local\request\contextlist; use core_privacy\local\request\approved_contextlist; /** * Mock core_user_data_provider for unit tests. * * @package core_privacy * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider { /** * Get metadata. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->add_subsystem_link('core_files'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist { $c = new \core_privacy\local\request\contextlist(); $c->add_system_context(); return $c; } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { } /** * Delete all data for all users in the specified context. * * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { } } fixtures/logo.png000064400000004622152265473400010077 0ustar00PNG  IHDRpatEXtSoftwareAdobe ImageReadyqe< 4IDATx[o\Y?Mv*~ w׀r-T @*A(iymy:*6J+y]B?TBZ-wwSUUHpf޹gウ3=9s~5&R:QUj$hȴX:Gh;1i=eиYCb۾Q%Tmsu?`KAmć'v+X["d-eO2:[D1TdTe@0 Hy-oiE` PV\Դt٤ LBTV!80p"[X8$ `r,CΟt. DXZL )ȥfFK"OQI9$+ЅX-:ʗ;X ޼uwoBj Pkuvjh ຎwuA.)@CQpǢq@wƘB+Ae$?Ϧ\`)ry"8ʕ[=U+I a cw#SbhhӒ%M|}d8;R/nVEggg~$ ZZ0 hA7mĮxs6m7M%-!j8䈩t݇ xjm֠^D8hC,c 1s`pm)އLV`pDFLNYzaN>oN ]OОZR O_v #v]U7Q`u̒;:P+صaG%x~^ǦTzg}VH>QҷGl`P9.Z2@rQF.CbӉ.!:z'zRSz9fF 0m"</b,vJ;S*g[ق,4rOM= D=v=pv ʯ(o4NT찪Nb}(?n3Ja}L'Ü X_A 0쐃q6&Bfl3yG 9h!%4IѹbU=-InLlB?8vJiXGۈ** GQ4|mJ\BqvCmK5YŗxHt1~LM|ƛp @ q+Pc%U]C"Z9l! O ^亠\ $F\^kKΦUh7#àĔ9Mt;Õ;RAz5;X ȡ~ssQa~% +/ Cmloˆlqd+)Pg>}=a[gjMU PS(SPidR~5Q}pk1]b܋4HL%f퐐9ɔٔO. Rm$HRQKs0ʊNke,kZA-7$<5KnT:IKֳŶ6I:. =#zVd8E'q 2e--Ϛ@d "|S9'=Y6l:҄B9<|&0 rk"n^ekxz 4ET9fkWUљ^+ФVl Om,~i҅9щT72̬2lS]\?ZwdˡP \^ӋcÅ&R33nvÚ&H֪IENDB`fixtures/mock_null_provider.php000064400000002745152265473400013043 0ustar00. /** * Test null provider using a fake plugin name. * * @package core_privacy * @copyright 2018 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_testcomponent2\privacy; /** * Mock null_provider for unit tests. * @copyright 2018 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string the reason for being a null provider. */ public static function get_reason(): string { return 'testcomponent2 null provider reason'; } } fixtures/mock_provider.php000064400000006532152265473400012007 0ustar00. /** * Test provider using a fake plugin name. * * @package core_privacy * @copyright 2018 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_testcomponent\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\contextlist; /** * Mock core_user_data_provider for unit tests. * @copyright 2018 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider { /** * @return array The array of metadata. */ public static function get_metadata(collection $collection): collection { $collection = new collection('testcomponent'); $collection->add_database_table('testtable', ['testfield1', 'testfield2'], 'testsummary'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { $cl = new contextlist(); $cl->add_from_sql("SELECT c.id FROM {context} c WHERE c.id = :id", ['id' => \context_system::instance()->id]); return $cl; } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { // This does nothing. We only want to confirm this can be called via the \core_privacy\manager. } /** * Delete all use data which matches the specified deletion criteria. * * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { // This does nothing. We only want to confirm this can be called via the \core_privacy\manager. } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { // This does nothing. We only want to confirm this can be called via the \core_privacy\manager. } } types_user_preference_test.php000064400000006440152265473400012730 0ustar00. namespace core_privacy; use core_privacy\local\metadata\types\user_preference; /** * Tests for the \core_privacy API's types\user_preference functionality. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\metadata\types\user_preference */ final class types_user_preference_test extends \advanced_testcase { /** * Ensure that warnings are thrown if string identifiers contain invalid characters. * * @dataProvider invalid_string_provider * @param string $name Name * @param string $summary Summary * @covers ::__construct */ public function test_invalid_configs($name, $summary): void { $record = new user_preference($name, $summary); $this->assertDebuggingCalled(); } /** * Ensure that warnings are not thrown if debugging is not enabled, even if string identifiers contain invalid characters. * * @dataProvider invalid_string_provider * @param string $name Name * @param string $summary Summary * @covers ::__construct */ public function test_invalid_configs_debug_normal($name, $summary): void { global $CFG; $this->resetAfterTest(); $CFG->debug = DEBUG_NORMAL; $record = new user_preference($name, $summary); $this->assertDebuggingNotCalled(); } /** * Ensure that no warnings are shown for valid combinations. * * @dataProvider valid_string_provider * @param string $name Name * @param string $summary Summary * @covers ::__construct */ public function test_valid_configs($name, $summary): void { $record = new user_preference($name, $summary); $this->assertDebuggingNotCalled(); } /** * Data provider with a list of invalid string identifiers. * * @return array */ public static function invalid_string_provider(): array { return [ 'Space in summary' => [ 'example', 'This table is used for purposes.', ], 'Comma in summary' => [ 'example', 'privacy,foo', ], ]; } /** * Data provider with a list of valid string identifiers. * * @return array */ public static function valid_string_provider(): array { return [ 'Valid combination' => [ 'example', 'privacy:example:valid', ], ]; } } types_subsystem_link_test.php000064400000006665152265473400012640 0ustar00. namespace core_privacy; use core_privacy\local\metadata\types\subsystem_link; /** * Tests for the \core_privacy API's types\subsystem_link functionality. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\metadata\types\subsystem_link */ final class types_subsystem_link_test extends \advanced_testcase { /** * Ensure that warnings are thrown if string identifiers contain invalid characters. * * @dataProvider invalid_string_provider * @param string $name Name * @param string $summary Summary * @covers ::__construct */ public function test_invalid_configs($name, $privacyfields, $summary): void { $record = new subsystem_link($name, $privacyfields, $summary); $this->assertDebuggingCalled(); } /** * Ensure that warnings are not thrown if debugging is not enabled, even if string identifiers contain invalid characters. * * @dataProvider invalid_string_provider * @param string $name Name * @param string $summary Summary * @covers ::__construct */ public function test_invalid_configs_debug_normal($name, $privacyfields, $summary): void { global $CFG; $this->resetAfterTest(); $CFG->debug = DEBUG_NORMAL; $record = new subsystem_link($name, $privacyfields, $summary); $this->assertDebuggingNotCalled(); } /** * Ensure that no warnings are shown for valid combinations. * * @dataProvider valid_string_provider * @param string $name Name * @param string $summary Summary * @covers ::__construct */ public function test_valid_configs($name, $privacyfields, $summary): void { $record = new subsystem_link($name, $privacyfields, $summary); $this->assertDebuggingNotCalled(); } /** * Data provider with a list of invalid string identifiers. * * @return array */ public static function invalid_string_provider(): array { return [ 'Space in summary' => [ 'example', [], 'This table is used for purposes.', ], 'Comma in summary' => [ 'example', [], 'privacy,foo', ], ]; } /** * Data provider with a list of valid string identifiers. * * @return array */ public static function valid_string_provider(): array { return [ 'Valid combination' => [ 'example', [], 'privacy:example:valid', ], ]; } } privacy/provider_advanced_test.php000064400000011073152265473400013462 0ustar00. namespace core_privacy\privacy; use core_privacy\manager; /** * Slow unit tests for all Privacy Providers that require database modifications. * * @package core_privacy * @copyright 2018 Andrew Nicols * @copyright 2025 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_advanced_test extends \advanced_testcase { /** * Returns a list of frankenstyle names of core components (plugins and subsystems). * * @return array the array of frankenstyle component names with the relevant class name. */ public static function get_component_list(): array { $components = ['core' => [ 'component' => 'core', 'classname' => manager::get_provider_classname_for_component('core'), ]]; // Get all plugins. $plugintypes = \core_component::get_plugin_types(); foreach ($plugintypes as $plugintype => $typedir) { $plugins = \core_component::get_plugin_list($plugintype); foreach ($plugins as $pluginname => $plugindir) { $frankenstyle = $plugintype . '_' . $pluginname; $components[$frankenstyle] = [ 'component' => $frankenstyle, 'classname' => manager::get_provider_classname_for_component($frankenstyle), ]; } } // Get all subsystems. foreach (\core_component::get_core_subsystems() as $name => $path) { if (isset($path)) { $frankenstyle = 'core_' . $name; $components[$frankenstyle] = [ 'component' => $frankenstyle, 'classname' => manager::get_provider_classname_for_component($frankenstyle), ]; } } return $components; } /** * Ensure that providers do not throw an error when processing a deleted user. * * @group plugin_checks * @dataProvider is_user_data_provider * @coversNothing * @param string $component */ public function test_component_understands_deleted_users($component): void { $this->resetAfterTest(); // Create a user. $user = $this->getDataGenerator()->create_user(); // Delete the user and their context. delete_user($user); $usercontext = \context_user::instance($user->id); $usercontext->delete(); $contextlist = manager::component_class_callback($component, \core_privacy\local\request\core_user_data_provider::class, 'get_contexts_for_userid', [$user->id]); $this->assertInstanceOf(\core_privacy\local\request\contextlist::class, $contextlist); } /** * List of providers which implement the core_user_data_provider. * * @return array */ public static function is_user_data_provider(): array { return array_map( fn ($data) => ['component' => $data['component']], array_filter(self::get_component_list(), function($component): bool { return static::component_implements( $component['classname'], \core_privacy\local\request\core_user_data_provider::class ); }), ); } /** * Checks whether the component's provider class implements the specified interface, either directly or as a grandchild. * * @param string $providerclass The name of the class to test. * @param string $interface the name of the interface we want to check. * @return bool Whether the class implements the interface. */ protected static function component_implements($providerclass, $interface) { if (class_exists($providerclass) && interface_exists($interface)) { return is_subclass_of($providerclass, $interface); } return false; } } contextlist_collection_test.php000064400000014506152265473400013125 0ustar00. /** * Unit Tests for a the collection of contextlists class * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\local\request\contextlist_collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; /** * Tests for the \core_privacy API's contextlist collection functionality. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\contextlist_collection */ final class contextlist_collection_test extends advanced_testcase { /** * A contextlist_collection should support the contextlist type. * * @covers ::add_contextlist */ public function test_supports_contextlist(): void { $uit = new contextlist_collection(1); $contextlist = new contextlist(); $contextlist->set_component('core_privacy'); $uit->add_contextlist($contextlist); $this->assertCount(1, $uit->get_contextlists()); } /** * A contextlist_collection should support the approved_contextlist type. * * @covers ::add_contextlist */ public function test_supports_approved_contextlist(): void { $uit = new contextlist_collection(1); $testuser = \core_user::get_user_by_username('admin'); $contextids = [3, 2, 1]; $uit->add_contextlist(new approved_contextlist($testuser, 'core_privacy', $contextids)); $this->assertCount(1, $uit->get_contextlists()); } /** * Ensure that get_contextlist_for_component returns the correct contextlist. * * @covers ::get_contextlist_for_component */ public function test_get_contextlist_for_component(): void { $uit = new contextlist_collection(1); $coretests = new contextlist(); $coretests->set_component('core_tests'); $uit->add_contextlist($coretests); $coreprivacy = new contextlist(); $coreprivacy->set_component('core_privacy'); $uit->add_contextlist($coreprivacy); // Note: This uses assertSame rather than assertEquals. // The former checks the actual object, whilst assertEquals only checks that they look the same. $this->assertSame($coretests, $uit->get_contextlist_for_component('core_tests')); $this->assertSame($coreprivacy, $uit->get_contextlist_for_component('core_privacy')); } /** * Ensure that get_contextlist_for_component does not die horribly when querying a non-existent component. * * @covers ::get_contextlist_for_component */ public function test_get_contextlist_for_component_not_found(): void { $uit = new contextlist_collection(1); $this->assertNull($uit->get_contextlist_for_component('core_tests')); } /** * Ensure that a duplicate contextlist in the collection throws an Exception. * * @covers ::add_contextlist */ public function test_duplicate_addition_throws(): void { $uit = new contextlist_collection(1); $coretests = new contextlist(); $coretests->set_component('core_tests'); $uit->add_contextlist($coretests); $this->expectException('moodle_exception'); $uit->add_contextlist($coretests); } /** * Ensure that the contextlist_collection is countable. * * @covers ::count */ public function test_countable(): void { $uit = new contextlist_collection(1); $contextlist = new contextlist(); $contextlist->set_component('test_example'); $uit->add_contextlist($contextlist); $contextlist = new contextlist(); $contextlist->set_component('test_another'); $uit->add_contextlist($contextlist); $this->assertCount(2, $uit); } /** * Ensure that the contextlist_collection iterates over the set of contextlists. * * @covers ::current * @covers ::key * @covers ::next * @covers ::rewind * @covers ::valid */ public function test_iteration(): void { $uit = new contextlist_collection(1); $testdata = []; $component = 'test_example'; $contextlist = new contextlist(); $contextlist->set_component($component); $uit->add_contextlist($contextlist); $testdata[$component] = $contextlist; $component = 'test_another'; $contextlist = new contextlist(); $contextlist->set_component($component); $uit->add_contextlist($contextlist); $testdata[$component] = $contextlist; $component = 'test_third'; $contextlist = new contextlist(); $contextlist->set_component($component); $uit->add_contextlist($contextlist); $testdata[$component] = $contextlist; foreach ($uit as $component => $list) { $this->assertEquals($testdata[$component], $list); } $this->assertCount(3, $uit); } /** * Test that the userid is correctly returned. * * @covers ::get_userid */ public function test_get_userid(): void { $uit = new contextlist_collection(1); $this->assertEquals(1, $uit->get_userid()); } /** * Test that an exception is thrown if a contextlist does not contain a component. */ public function test_add_without_component(): void { $uit = new contextlist_collection(1); $this->expectException(moodle_exception::class); $uit->add_contextlist(new contextlist()); } } moodle_content_writer_test.php000064400000175116152265473400012744 0ustar00. /** * Unit Tests for the Moodle Content Writer. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\local\request\writer; use \core_privacy\local\request\moodle_content_writer; /** * Tests for the \core_privacy API's moodle_content_writer functionality. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\moodle_content_writer */ final class moodle_content_writer_test extends advanced_testcase { /** * Test that exported data is saved correctly within the system context. * * @dataProvider export_data_provider * @param \stdClass $data Data * @covers ::export_data */ public function test_export_data($data): void { $context = \context_system::instance(); $subcontext = []; $writer = $this->get_writer_instance() ->set_context($context) ->export_data($subcontext, $data); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'data.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertEquals($data, $expanded); } /** * Test that exported data is saved correctly for context/subcontext. * * @dataProvider export_data_provider * @param \stdClass $data Data * @covers ::export_data */ public function test_export_data_different_context($data): void { $context = \context_user::instance(\core_user::get_user_by_username('admin')->id); $subcontext = ['sub', 'context']; $writer = $this->get_writer_instance() ->set_context($context) ->export_data($subcontext, $data); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'data.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertEquals($data, $expanded); } /** * Test that exported is saved within the correct directory locations. * * @covers ::export_data */ public function test_export_data_writes_to_multiple_context(): void { $subcontext = ['sub', 'context']; $systemcontext = \context_system::instance(); $systemdata = (object) [ 'belongsto' => 'system', ]; $usercontext = \context_user::instance(\core_user::get_user_by_username('admin')->id); $userdata = (object) [ 'belongsto' => 'user', ]; $writer = $this->get_writer_instance(); $writer ->set_context($systemcontext) ->export_data($subcontext, $systemdata); $writer ->set_context($usercontext) ->export_data($subcontext, $userdata); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($systemcontext, $subcontext, 'data.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertEquals($systemdata, $expanded); $contextpath = $this->get_context_path($usercontext, $subcontext, 'data.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertEquals($userdata, $expanded); } /** * Test that multiple writes to the same location cause the latest version to be written. * * @covers ::export_data */ public function test_export_data_multiple_writes_same_context(): void { $subcontext = ['sub', 'context']; $systemcontext = \context_system::instance(); $originaldata = (object) [ 'belongsto' => 'system', ]; $newdata = (object) [ 'abc' => 'def', ]; $writer = $this->get_writer_instance(); $writer ->set_context($systemcontext) ->export_data($subcontext, $originaldata); $writer ->set_context($systemcontext) ->export_data($subcontext, $newdata); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($systemcontext, $subcontext, 'data.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertEquals($newdata, $expanded); } /** * Data provider for exporting user data. */ public static function export_data_provider(): array { return [ 'basic' => [ (object) [ 'example' => (object) [ 'key' => 'value', ], ], ], ]; } /** * Test that metadata can be set. * * @dataProvider export_metadata_provider * @param string $key Key * @param string $value Value * @param string $description Description * @covers ::export_metadata */ public function test_export_metadata($key, $value, $description): void { $context = \context_system::instance(); $subcontext = ['a', 'b', 'c']; $writer = $this->get_writer_instance() ->set_context($context) ->export_metadata($subcontext, $key, $value, $description); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $this->assertEquals($value, $expanded->$key->value); $this->assertEquals($description, $expanded->$key->description); } /** * Test that metadata can be set additively. * * @covers ::export_metadata */ public function test_export_metadata_additive(): void { $context = \context_system::instance(); $subcontext = []; $writer = $this->get_writer_instance(); $writer ->set_context($context) ->export_metadata($subcontext, 'firstkey', 'firstvalue', 'firstdescription'); $writer ->set_context($context) ->export_metadata($subcontext, 'secondkey', 'secondvalue', 'seconddescription'); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json'); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->firstkey)); $this->assertEquals('firstvalue', $expanded->firstkey->value); $this->assertEquals('firstdescription', $expanded->firstkey->description); $this->assertTrue(isset($expanded->secondkey)); $this->assertEquals('secondvalue', $expanded->secondkey->value); $this->assertEquals('seconddescription', $expanded->secondkey->description); } /** * Test that metadata can be set additively. * * @covers ::export_metadata */ public function test_export_metadata_to_multiple_contexts(): void { $systemcontext = \context_system::instance(); $usercontext = \context_user::instance(\core_user::get_user_by_username('admin')->id); $subcontext = []; $writer = $this->get_writer_instance(); $writer ->set_context($systemcontext) ->export_metadata($subcontext, 'firstkey', 'firstvalue', 'firstdescription') ->export_metadata($subcontext, 'secondkey', 'secondvalue', 'seconddescription'); $writer ->set_context($usercontext) ->export_metadata($subcontext, 'firstkey', 'alternativevalue', 'alternativedescription') ->export_metadata($subcontext, 'thirdkey', 'thirdvalue', 'thirddescription'); $fileroot = $this->fetch_exported_content($writer); $systemcontextpath = $this->get_context_path($systemcontext, $subcontext, 'metadata.json'); $this->assertTrue($fileroot->hasChild($systemcontextpath)); $json = $fileroot->getChild($systemcontextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->firstkey)); $this->assertEquals('firstvalue', $expanded->firstkey->value); $this->assertEquals('firstdescription', $expanded->firstkey->description); $this->assertTrue(isset($expanded->secondkey)); $this->assertEquals('secondvalue', $expanded->secondkey->value); $this->assertEquals('seconddescription', $expanded->secondkey->description); $this->assertFalse(isset($expanded->thirdkey)); $usercontextpath = $this->get_context_path($usercontext, $subcontext, 'metadata.json'); $this->assertTrue($fileroot->hasChild($usercontextpath)); $json = $fileroot->getChild($usercontextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->firstkey)); $this->assertEquals('alternativevalue', $expanded->firstkey->value); $this->assertEquals('alternativedescription', $expanded->firstkey->description); $this->assertFalse(isset($expanded->secondkey)); $this->assertTrue(isset($expanded->thirdkey)); $this->assertEquals('thirdvalue', $expanded->thirdkey->value); $this->assertEquals('thirddescription', $expanded->thirdkey->description); } /** * Data provider for exporting user metadata. * * return array */ public static function export_metadata_provider(): array { return [ 'basic' => [ 'key', 'value', 'This is a description', ], 'valuewithspaces' => [ 'key', 'value has mixed', 'This is a description', ], 'encodedvalue' => [ 'key', base64_encode('value has mixed'), 'This is a description', ], ]; } /** * Exporting a single stored_file should cause that file to be output in the files directory. * * @covers ::export_area_files */ public function test_export_area_files(): void { $this->resetAfterTest(); $context = \context_system::instance(); $fs = get_file_storage(); // Add two files to core_privacy::tests::0. $files = []; $file = (object) [ 'component' => 'core_privacy', 'filearea' => 'tests', 'itemid' => 0, 'path' => '/', 'name' => 'a.txt', 'content' => 'Test file 0', ]; $files[] = $file; $file = (object) [ 'component' => 'core_privacy', 'filearea' => 'tests', 'itemid' => 0, 'path' => '/sub/', 'name' => 'b.txt', 'content' => 'Test file 1', ]; $files[] = $file; // One with a different itemid. $file = (object) [ 'component' => 'core_privacy', 'filearea' => 'tests', 'itemid' => 1, 'path' => '/', 'name' => 'c.txt', 'content' => 'Other', ]; $files[] = $file; // One with a different filearea. $file = (object) [ 'component' => 'core_privacy', 'filearea' => 'alternative', 'itemid' => 0, 'path' => '/', 'name' => 'd.txt', 'content' => 'Alternative', ]; $files[] = $file; // One with a different component. $file = (object) [ 'component' => 'core', 'filearea' => 'tests', 'itemid' => 0, 'path' => '/', 'name' => 'e.txt', 'content' => 'Other tests', ]; $files[] = $file; foreach ($files as $file) { $record = [ 'contextid' => $context->id, 'component' => $file->component, 'filearea' => $file->filearea, 'itemid' => $file->itemid, 'filepath' => $file->path, 'filename' => $file->name, ]; $file->namepath = '/' . $file->filearea . '/' . ($file->itemid ?: '') . $file->path . $file->name; $file->storedfile = $fs->create_file_from_string($record, $file->content); } $writer = $this->get_writer_instance() ->set_context($context) ->export_area_files([], 'core_privacy', 'tests', 0); $fileroot = $this->fetch_exported_content($writer); $firstfiles = array_slice($files, 0, 2); foreach ($firstfiles as $file) { $contextpath = $this->get_context_path($context, ['_files'], $file->namepath); $this->assertTrue($fileroot->hasChild($contextpath)); $this->assertEquals($file->content, $fileroot->getChild($contextpath)->getContent()); } $otherfiles = array_slice($files, 2); foreach ($otherfiles as $file) { $contextpath = $this->get_context_path($context, ['_files'], $file->namepath); $this->assertFalse($fileroot->hasChild($contextpath)); } } /** * Exporting a single stored_file should cause that file to be output in the files directory. * * @dataProvider export_file_provider * @param string $filearea File area * @param int $itemid Item ID * @param string $filepath File path * @param string $filename File name * @param string $content Content * * @covers ::export_file */ public function test_export_file($filearea, $itemid, $filepath, $filename, $content): void { $this->resetAfterTest(); $context = \context_system::instance(); $filenamepath = '/' . $filearea . '/' . ($itemid ? '_' . $itemid : '') . $filepath . $filename; $filerecord = array( 'contextid' => $context->id, 'component' => 'core_privacy', 'filearea' => $filearea, 'itemid' => $itemid, 'filepath' => $filepath, 'filename' => $filename, ); $fs = get_file_storage(); $file = $fs->create_file_from_string($filerecord, $content); $writer = $this->get_writer_instance() ->set_context($context) ->export_file([], $file); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, ['_files'], $filenamepath); $this->assertTrue($fileroot->hasChild($contextpath)); $this->assertEquals($content, $fileroot->getChild($contextpath)->getContent()); } /** * Data provider for the test_export_file function. * * @return array */ public static function export_file_provider(): array { return [ 'basic' => [ 'intro', 0, '/', 'testfile.txt', 'An example file content', ], 'longpath' => [ 'attachments', '12', '/path/within/a/path/within/a/path/', 'testfile.txt', 'An example file content', ], 'pathwithspaces' => [ 'intro', 0, '/path with/some spaces/', 'testfile.txt', 'An example file content', ], 'filewithspaces' => [ 'submission_attachments', 1, '/path with/some spaces/', 'test file.txt', 'An example file content', ], 'image' => [ 'intro', 0, '/', 'logo.png', file_get_contents(self::get_fixture_path('core_privacy', 'logo.png')), ], 'UTF8' => [ 'submission_content', 2, '/Žluťoučký/', 'koníček.txt', 'koníček', ], 'EUC-JP' => [ 'intro', 0, '/言語設定/', '言語設定.txt', '言語設定', ], ]; } /** * User preferences can be exported against a user. * * @dataProvider export_user_preference_provider * @param string $component Component * @param string $key Key * @param string $value Value * @param string $desc Description * @covers ::export_user_preference */ public function test_export_user_preference_context_user($component, $key, $value, $desc): void { $admin = \core_user::get_user_by_username('admin'); $writer = $this->get_writer_instance(); $context = \context_user::instance($admin->id); $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $key, $value, $desc); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals($value, $data->value); $this->assertEquals($desc, $data->description); } /** * User preferences can be exported against a course category. * * @dataProvider export_user_preference_provider * @param string $component Component * @param string $key Key * @param string $value Value * @param string $desc Description * @covers ::export_user_preference */ public function test_export_user_preference_context_coursecat($component, $key, $value, $desc): void { global $DB; $categories = $DB->get_records('course_categories'); $firstcategory = reset($categories); $context = \context_coursecat::instance($firstcategory->id); $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $key, $value, $desc); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals($value, $data->value); $this->assertEquals($desc, $data->description); } /** * User preferences can be exported against a course. * * @dataProvider export_user_preference_provider * @param string $component Component * @param string $key Key * @param string $value Value * @param string $desc Description * @covers ::export_user_preference */ public function test_export_user_preference_context_course($component, $key, $value, $desc): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $key, $value, $desc); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals($value, $data->value); $this->assertEquals($desc, $data->description); } /** * User preferences can be exported against a module context. * * @dataProvider export_user_preference_provider * @param string $component Component * @param string $key Key * @param string $value Value * @param string $desc Description * @covers ::export_user_preference */ public function test_export_user_preference_context_module($component, $key, $value, $desc): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $context = \context_module::instance($forum->cmid); $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $key, $value, $desc); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals($value, $data->value); $this->assertEquals($desc, $data->description); } /** * User preferences can not be exported against a block context. * * @dataProvider export_user_preference_provider * @param string $component Component * @param string $key Key * @param string $value Value * @param string $desc Description * @covers ::export_user_preference */ public function test_export_user_preference_context_block($component, $key, $value, $desc): void { global $DB; $blocks = $DB->get_records('block_instances'); $block = reset($blocks); $context = \context_block::instance($block->id); $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $key, $value, $desc); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals($value, $data->value); $this->assertEquals($desc, $data->description); } /** * Writing user preferences for two different blocks with the same name and * same parent context should generate two different context paths and export * files. * * @covers ::export_user_preference */ public function test_export_user_preference_context_block_multiple_instances(): void { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $coursecontext = context_course::instance($course->id); $block1 = $generator->create_block('online_users', ['parentcontextid' => $coursecontext->id]); $block2 = $generator->create_block('online_users', ['parentcontextid' => $coursecontext->id]); $block1context = context_block::instance($block1->id); $block2context = context_block::instance($block2->id); $component = 'block'; $desc = 'test preference'; $block1key = 'block1key'; $block1value = 'block1value'; $block2key = 'block2key'; $block2value = 'block2value'; $writer = $this->get_writer_instance(); // Confirm that we have two different block contexts with the same name // and the same parent context id. $this->assertNotEquals($block1context->id, $block2context->id); $this->assertEquals($block1context->get_context_name(), $block2context->get_context_name()); $this->assertEquals($block1context->get_parent_context()->id, $block2context->get_parent_context()->id); $retrieveexport = function($context) use ($writer, $component) { $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); return json_decode($json); }; $writer->set_context($block1context) ->export_user_preference($component, $block1key, $block1value, $desc); $writer->set_context($block2context) ->export_user_preference($component, $block2key, $block2value, $desc); $block1export = $retrieveexport($block1context); $block2export = $retrieveexport($block2context); // Confirm that the exports didn't write to the same file. $this->assertTrue(isset($block1export->$block1key)); $this->assertTrue(isset($block2export->$block2key)); $this->assertFalse(isset($block1export->$block2key)); $this->assertFalse(isset($block2export->$block1key)); $this->assertEquals($block1value, $block1export->$block1key->value); $this->assertEquals($block2value, $block2export->$block2key->value); } /** * User preferences can be exported against the system. * * @dataProvider export_user_preference_provider * @param string $component Component * @param string $key Key * @param string $value Value * @param string $desc Description * * @covers ::export_user_preference */ public function test_export_user_preference_context_system($component, $key, $value, $desc): void { $context = \context_system::instance(); $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $key, $value, $desc); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals($value, $data->value); $this->assertEquals($desc, $data->description); } /** * User preferences can be exported against the system. * * @covers ::export_user_preference */ public function test_export_multiple_user_preference_context_system(): void { $context = \context_system::instance(); $writer = $this->get_writer_instance(); $component = 'core_privacy'; $writer ->set_context($context) ->export_user_preference($component, 'key1', 'val1', 'desc1') ->export_user_preference($component, 'key2', 'val2', 'desc2'); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->key1)); $data = $expanded->key1; $this->assertEquals('val1', $data->value); $this->assertEquals('desc1', $data->description); $this->assertTrue(isset($expanded->key2)); $data = $expanded->key2; $this->assertEquals('val2', $data->value); $this->assertEquals('desc2', $data->description); } /** * User preferences can be exported against the system. * * @covers ::export_user_preference */ public function test_export_user_preference_replace(): void { $context = \context_system::instance(); $writer = $this->get_writer_instance(); $component = 'core_privacy'; $key = 'key'; $writer ->set_context($context) ->export_user_preference($component, $key, 'val1', 'desc1'); $writer ->set_context($context) ->export_user_preference($component, $key, 'val2', 'desc2'); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertTrue($fileroot->hasChild($contextpath)); $json = $fileroot->getChild($contextpath)->getContent(); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$key)); $data = $expanded->$key; $this->assertEquals('val2', $data->value); $this->assertEquals('desc2', $data->description); } /** * Provider for various user preferences. * * @return array */ public static function export_user_preference_provider(): array { return [ 'basic' => [ 'core_privacy', 'onekey', 'value', 'description', ], 'encodedvalue' => [ 'core_privacy', 'donkey', base64_encode('value'), 'description', ], 'long description' => [ 'core_privacy', 'twokey', 'value', 'This is a much longer description which actually states what this is used for. Blah blah blah.', ], ]; } /** * Test that exported data is human readable. * * @dataProvider unescaped_unicode_export_provider * @param string $text * @covers ::export_data */ public function test_export_data_unescaped_unicode($text): void { $context = \context_system::instance(); $subcontext = []; $data = (object) ['key' => $text]; $writer = $this->get_writer_instance() ->set_context($context) ->export_data($subcontext, $data); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'data.json'); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertMatchesRegularExpression("/$text/", $json); $expanded = json_decode($json); $this->assertEquals($data, $expanded); } /** * Test that exported metadata is human readable. * * @dataProvider unescaped_unicode_export_provider * @param string $text * @covers ::export_metadata */ public function test_export_metadata_unescaped_unicode($text): void { $context = \context_system::instance(); $subcontext = ['a', 'b', 'c']; $writer = $this->get_writer_instance() ->set_context($context) ->export_metadata($subcontext, $text, $text, $text); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json'); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertMatchesRegularExpression("/$text.*$text.*$text/is", $json); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$text)); $this->assertEquals($text, $expanded->$text->value); $this->assertEquals($text, $expanded->$text->description); } /** * Test that exported related data is human readable. * * @dataProvider unescaped_unicode_export_provider * @param string $text * @covers ::export_related_data */ public function test_export_related_data_unescaped_unicode($text): void { $context = \context_system::instance(); $subcontext = []; $data = (object) ['key' => $text]; $writer = $this->get_writer_instance() ->set_context($context) ->export_related_data($subcontext, 'name', $data); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'name.json'); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertMatchesRegularExpression("/$text/", $json); $expanded = json_decode($json); $this->assertEquals($data, $expanded); } /** * Test that exported related data name is properly cleaned * * @covers ::export_related_data */ public function test_export_related_data_clean_name(): void { $context = \context_system::instance(); $subcontext = []; $data = (object) ['foo' => 'bar']; $name = 'Bad/chars:>'; $writer = $this->get_writer_instance() ->set_context($context) ->export_related_data($subcontext, $name, $data); $nameclean = clean_param($name, PARAM_FILE); $contextpath = $this->get_context_path($context, $subcontext, "{$nameclean}.json"); $expectedpath = "System _.{$context->id}/Badchars.json"; $this->assertEquals($expectedpath, $contextpath); $fileroot = $this->fetch_exported_content($writer); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertEquals($data, json_decode($json)); } /** * Test that exported user preference is human readable. * * @dataProvider unescaped_unicode_export_provider * @param string $text * @covers ::export_user_preference */ public function test_export_user_preference_unescaped_unicode($text): void { $context = \context_system::instance(); $component = 'core_privacy'; $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $text, $text, $text); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertMatchesRegularExpression("/$text.*$text.*$text/is", $json); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$text)); $this->assertEquals($text, $expanded->$text->value); $this->assertEquals($text, $expanded->$text->description); } /** * Provider for various user preferences. * * @return array */ public static function unescaped_unicode_export_provider(): array { return [ 'Unicode' => ['ةكءيٓ‌پچژکگیٹڈڑہھےâîûğŞAaÇÖáǽ你好!'], ]; } /** * Test that exported data subcontext is properly cleaned * * @covers ::export_data */ public function test_export_data_clean_subcontext(): void { $context = \context_system::instance(); $subcontext = ['Something/weird', 'More/bad:>', 'Bad&chars:>']; $data = (object) ['foo' => 'bar']; $writer = $this->get_writer_instance() ->set_context($context) ->export_data($subcontext, $data); $contextpath = $this->get_context_path($context, $subcontext, 'data.json'); $expectedpath = "System _.{$context->id}/Something/weird/More/bad/Badchars/data.json"; $this->assertEquals($expectedpath, $contextpath); $fileroot = $this->fetch_exported_content($writer); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertEquals($data, json_decode($json)); } /** * Test that exported data is shortened when exceeds the limit. * * @dataProvider long_filename_provider * @param string $longtext * @param string $expected * @param string $text * * @covers ::export_data */ public function test_export_data_long_filename($longtext, $expected, $text): void { $context = \context_system::instance(); $subcontext = [$longtext]; $data = (object) ['key' => $text]; $writer = $this->get_writer_instance() ->set_context($context) ->export_data($subcontext, $data); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'data.json'); $expectedpath = "System _.{$context->id}/{$expected}/data.json"; $this->assertEquals($expectedpath, $contextpath); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertMatchesRegularExpression("/$text/", $json); $expanded = json_decode($json); $this->assertEquals($data, $expanded); } /** * Test that exported related data is shortened when exceeds the limit. * * @dataProvider long_filename_provider * @param string $longtext * @param string $expected * @param string $text * * @covers ::export_related_data */ public function test_export_related_data_long_filename($longtext, $expected, $text): void { $context = \context_system::instance(); $subcontext = [$longtext]; $data = (object) ['key' => $text]; $writer = $this->get_writer_instance() ->set_context($context) ->export_related_data($subcontext, 'name', $data); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'name.json'); $expectedpath = "System _.{$context->id}/{$expected}/name.json"; $this->assertEquals($expectedpath, $contextpath); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertMatchesRegularExpression("/$text/", $json); $expanded = json_decode($json); $this->assertEquals($data, $expanded); } /** * Test that exported metadata is shortened when exceeds the limit. * * @dataProvider long_filename_provider * @param string $longtext * @param string $expected * @param string $text */ public function test_export_metadata_long_filename($longtext, $expected, $text): void { $context = \context_system::instance(); $subcontext = [$longtext]; $data = (object) ['key' => $text]; $writer = $this->get_writer_instance() ->set_context($context) ->export_metadata($subcontext, $text, $text, $text); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json'); $expectedpath = "System _.{$context->id}/{$expected}/metadata.json"; $this->assertEquals($expectedpath, $contextpath); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertMatchesRegularExpression("/$text.*$text.*$text/is", $json); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$text)); $this->assertEquals($text, $expanded->$text->value); $this->assertEquals($text, $expanded->$text->description); } /** * Test that exported user preference is shortened when exceeds the limit. * * @dataProvider long_filename_provider * @param string $longtext * @param string $expected * @param string $text */ public function test_export_user_preference_long_filename($longtext, $expected, $text): void { $this->resetAfterTest(); if (!array_key_exists('json', core_filetypes::get_types())) { // Add json as mime type to avoid lose the extension when shortening filenames. core_filetypes::add_type('json', 'application/json', 'archive', [], '', 'JSON file archive'); } $context = \context_system::instance(); $expectedpath = "System _.{$context->id}/User preferences/{$expected}.json"; $component = $longtext; $writer = $this->get_writer_instance() ->set_context($context) ->export_user_preference($component, $text, $text, $text); $fileroot = $this->fetch_exported_content($writer); $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); $this->assertEquals($expectedpath, $contextpath); $json = $fileroot->getChild($contextpath)->getContent(); $this->assertMatchesRegularExpression("/$text.*$text.*$text/is", $json); $expanded = json_decode($json); $this->assertTrue(isset($expanded->$text)); $this->assertEquals($text, $expanded->$text->value); $this->assertEquals($text, $expanded->$text->description); } /** * Provider for long filenames. * * @return array */ public static function long_filename_provider(): array { return [ 'More than 100 characters' => [ 'Etiam sit amet dui vel leo blandit viverra. Proin viverra suscipit velit. Aenean efficitur suscipit nibh nec suscipit', 'Etiam sit amet dui vel leo blandit viverra. Proin viverra suscipit velit. Aenean effici - 22f7a5030d', 'value', ], ]; } /** * Get a fresh content writer. * * @return moodle_content_writer */ public function get_writer_instance() { $factory = $this->createMock(writer::class); return new moodle_content_writer($factory); } /** * Fetch the exported content for inspection. * * @param moodle_content_writer $writer * @return \org\bovigo\vfs\vfsStreamDirectory */ protected function fetch_exported_content(moodle_content_writer $writer) { $export = $writer ->set_context(\context_system::instance()) ->finalise_content(); $fileroot = \org\bovigo\vfs\vfsStream::setup('root'); $target = \org\bovigo\vfs\vfsStream::url('root'); $fp = get_file_packer(); $fp->extract_to_pathname($export, $target); return $fileroot; } /** * Determine the path for the current context. * * Note: This is a wrapper around the real function. * * @param \context $context The context being written * @param array $subcontext The subcontext path * @param string $name THe name of the file target * @return array The context path. */ protected function get_context_path($context, $subcontext = null, $name = '') { $rc = new ReflectionClass(moodle_content_writer::class); $writer = $this->get_writer_instance(); $writer->set_context($context); if (null === $subcontext) { $rcm = $rc->getMethod('get_context_path'); $path = $rcm->invoke($writer); } else { $rcm = $rc->getMethod('get_path'); $path = $rcm->invoke($writer, $subcontext, $name); } // PHPUnit uses mikey179/vfsStream which is a stream wrapper for a virtual file system that uses '/' // as the directory separator. $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); return $path; } /** * Test correct rewriting of @@PLUGINFILE@@ in the exported contents. * * @dataProvider rewrite_pluginfile_urls_provider * @param string $filearea The filearea within that component. * @param int $itemid Which item those files belong to. * @param string $input Raw text as stored in the database. * @param string $expectedoutput Expected output of URL rewriting. * @covers ::rewrite_pluginfile_urls */ public function test_rewrite_pluginfile_urls($filearea, $itemid, $input, $expectedoutput): void { $writer = $this->get_writer_instance(); $writer->set_context(\context_system::instance()); $realoutput = $writer->rewrite_pluginfile_urls([], 'core_test', $filearea, $itemid, $input); $this->assertEquals($expectedoutput, $realoutput); } /** * Provides testable sample data for {@link self::test_rewrite_pluginfile_urls()}. * * @return array */ public static function rewrite_pluginfile_urls_provider(): array { return [ 'nullcontent' => [ 'intro', 0, null, '', ], 'emptycontent' => [ 'intro', 0, '', '', ], 'zeroitemid' => [ 'intro', 0, '

', '

', ], 'nonzeroitemid' => [ 'submission_content', 34, '

First

', '

First

', ], 'withfilepath' => [ 'post_content', 9889, 'Click here!', 'Click here!', ], ]; } public function test_export_html_functions(): void { $this->resetAfterTest(); $data = (object) ['key' => 'value']; $context = \context_system::instance(); $subcontext = []; $writer = $this->get_writer_instance() ->set_context($context) ->export_data($subcontext, (object) $data); $writer->set_context($context)->export_data(['paper'], $data); $coursecategory = $this->getDataGenerator()->create_category(); $categorycontext = \context_coursecat::instance($coursecategory->id); $course = $this->getDataGenerator()->create_course(); $misccoursecxt = \context_coursecat::instance($course->category); $coursecontext = \context_course::instance($course->id); $cm = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]); $modulecontext = \context_module::instance($cm->cmid); $writer->set_context($modulecontext)->export_data([], $data); $writer->set_context($coursecontext)->export_data(['grades'], $data); $writer->set_context($categorycontext)->export_data([], $data); $writer->set_context($context)->export_data([get_string('privacy:path:logs', 'tool_log'), 'Standard log'], $data); // Add a file. $fs = get_file_storage(); $file = (object) [ 'component' => 'core_privacy', 'filearea' => 'tests', 'itemid' => 0, 'path' => '/', 'name' => 'a.txt', 'content' => 'Test file 0', ]; $record = [ 'contextid' => $context->id, 'component' => $file->component, 'filearea' => $file->filearea, 'itemid' => $file->itemid, 'filepath' => $file->path, 'filename' => $file->name, ]; $file->namepath = '/' . $file->filearea . '/' . ($file->itemid ?: '') . $file->path . $file->name; $file->storedfile = $fs->create_file_from_string($record, $file->content); $writer->set_context($context)->export_area_files([], 'core_privacy', 'tests', 0); list($tree, $treelist, $indexdata) = phpunit_util::call_internal_method($writer, 'prepare_for_export', [], '\core_privacy\local\request\moodle_content_writer'); $expectedtreeoutput = [ 'System _.1' => [ 'data.json', 'paper' => 'data.json', 'Category Category 1 _.' . $misccoursecxt->id => [ 'Course Test course 1 _.' . $coursecontext->id => [ 'Assignment Assignment 1 _.' . $modulecontext->id => 'data.json', 'grades' => 'data.json' ] ], 'Category Course category 1 _.' . $categorycontext->id => 'data.json', '_files' => [ 'tests' => 'a.txt' ], 'Logs' => [ 'Standard log' => 'data.json' ] ] ]; $this->assertEquals($expectedtreeoutput, $tree); $expectedlistoutput = [ 'System _.1/data.json' => 'data_file_1', 'System _.1/paper/data.json' => 'data_file_2', 'System _.1/Category Category 1 _.' . $misccoursecxt->id . '/Course Test course 1 _.' . $coursecontext->id . '/Assignment Assignment 1 _.' . $modulecontext->id . '/data.json' => 'data_file_3', 'System _.1/Category Category 1 _.' . $misccoursecxt->id . '/Course Test course 1 _.' . $coursecontext->id . '/grades/data.json' => 'data_file_4', 'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.json' => 'data_file_5', 'System _.1/_files/tests/a.txt' => 'No var', 'System _.1/Logs/Standard log/data.json' => 'data_file_6' ]; $this->assertEquals($expectedlistoutput, $treelist); $expectedindex = [ 'data_file_1' => 'System _.1/data.js', 'data_file_2' => 'System _.1/paper/data.js', 'data_file_3' => 'System _.1/Category Category 1 _.' . $misccoursecxt->id . '/Course Test course 1 _.' . $coursecontext->id . '/Assignment Assignment 1 _.' . $modulecontext->id . '/data.js', 'data_file_4' => 'System _.1/Category Category 1 _.' . $misccoursecxt->id . '/Course Test course 1 _.' . $coursecontext->id . '/grades/data.js', 'data_file_5' => 'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.js', 'data_file_6' => 'System _.1/Logs/Standard log/data.js' ]; $this->assertEquals($expectedindex, $indexdata); $richtree = phpunit_util::call_internal_method($writer, 'make_tree_object', [$tree, $treelist], '\core_privacy\local\request\moodle_content_writer'); // This is a big one. $expectedrichtree = [ 'System _.1' => (object) [ 'itemtype' => 'treeitem', 'name' => 'System ', 'context' => \context_system::instance(), 'children' => [ (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_1' ], 'paper' => (object) [ 'itemtype' => 'treeitem', 'name' => 'paper', 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_2' ] ] ], 'Category Category 1 _.' . $misccoursecxt->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Category Category 1 ', 'context' => $misccoursecxt, 'children' => [ 'Course Test course 1 _.' . $coursecontext->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Course Test course 1 ', 'context' => $coursecontext, 'children' => [ 'Assignment Assignment 1 _.' . $modulecontext->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Assignment Assignment 1 ', 'context' => $modulecontext, 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_3' ] ] ], 'grades' => (object) [ 'itemtype' => 'treeitem', 'name' => 'grades', 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_4' ] ] ] ] ] ] ], 'Category Course category 1 _.' . $categorycontext->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Category Course category 1 ', 'context' => $categorycontext, 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_5' ] ] ], '_files' => (object) [ 'itemtype' => 'treeitem', 'name' => '_files', 'children' => [ 'tests' => (object) [ 'itemtype' => 'treeitem', 'name' => 'tests', 'children' => [ 'a.txt' => (object) [ 'name' => 'a.txt', 'itemtype' => 'item', 'url' => new \moodle_url('System _.1/_files/tests/a.txt') ] ] ] ] ], 'Logs' => (object) [ 'itemtype' => 'treeitem', 'name' => 'Logs', 'children' => [ 'Standard log' => (object) [ 'itemtype' => 'treeitem', 'name' => 'Standard log', 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_6' ] ] ] ] ] ] ] ]; $this->assertEquals($expectedrichtree, $richtree); // The phpunit_util::call_internal_method() method doesn't allow for referenced parameters so we have this joyful code // instead to do the same thing, but with references working obviously. $funfunction = function($object, $data) { return $object->sort_my_list($data); }; $funfunction = Closure::bind($funfunction, null, $writer); $funfunction($writer, $richtree); // This is a big one. $expectedsortedtree = [ 'System _.1' => (object) [ 'itemtype' => 'treeitem', 'name' => 'System ', 'context' => \context_system::instance(), 'children' => [ 'Category Category 1 _.' . $misccoursecxt->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Category Category 1 ', 'context' => $misccoursecxt, 'children' => [ 'Course Test course 1 _.' . $coursecontext->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Course Test course 1 ', 'context' => $coursecontext, 'children' => [ 'Assignment Assignment 1 _.' . $modulecontext->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Assignment Assignment 1 ', 'context' => $modulecontext, 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_3' ] ] ], 'grades' => (object) [ 'itemtype' => 'treeitem', 'name' => 'grades', 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_4' ] ] ] ] ] ] ], 'Category Course category 1 _.' . $categorycontext->id => (object) [ 'itemtype' => 'treeitem', 'name' => 'Category Course category 1 ', 'context' => $categorycontext, 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_5' ] ] ], '_files' => (object) [ 'itemtype' => 'treeitem', 'name' => '_files', 'children' => [ 'tests' => (object) [ 'itemtype' => 'treeitem', 'name' => 'tests', 'children' => [ 'a.txt' => (object) [ 'name' => 'a.txt', 'itemtype' => 'item', 'url' => new \moodle_url('System _.1/_files/tests/a.txt') ] ] ] ] ], 'Logs' => (object) [ 'itemtype' => 'treeitem', 'name' => 'Logs', 'children' => [ 'Standard log' => (object) [ 'itemtype' => 'treeitem', 'name' => 'Standard log', 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_6' ] ] ] ] ], 'paper' => (object) [ 'itemtype' => 'treeitem', 'name' => 'paper', 'children' => [ 'data.json' => (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_2' ] ] ], (object) [ 'name' => 'data.json', 'itemtype' => 'item', 'datavar' => 'data_file_1' ] ] ] ]; $this->assertEquals($expectedsortedtree, $richtree); } } contextlist_base_test.php000064400000022072152265473400011701 0ustar00. /** * Unit Tests for the abstract contextlist Class * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\local\request\contextlist_base; /** * Tests for the \core_privacy API's contextlist base functionality. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\contextlist_base */ final class contextlist_base_test extends advanced_testcase { /** * Ensure that get_contextids returns the list of unique contextids. * * @dataProvider get_contextids_provider * @param array $input List of context IDs * @param array $expected list of contextids * @param int $count Expected count * @covers ::get_contextids */ public function test_get_contextids($input, $expected, $count): void { $uit = new test_contextlist_base(); $uit->set_contextids($input); $result = $uit->get_contextids(); $this->assertCount($count, $result); // Note: Array order is not guaranteed and should not matter. foreach ($expected as $contextid) { $this->assertNotFalse(array_search($contextid, $result)); } } /** * Provider for the list of contextids. * * @return array */ public static function get_contextids_provider(): array { return [ 'basic' => [ [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], 5, ], 'duplicates' => [ [1, 1, 2, 2, 3, 4, 5], [1, 2, 3, 4, 5], 5, ], 'Mixed order with duplicates' => [ [5, 4, 2, 5, 4, 1, 3, 4, 1, 5, 5, 5, 2, 4, 1, 2], [1, 2, 3, 4, 5], 5, ], ]; } /** * Ensure that get_contexts returns the correct list of contexts. * * @covers ::get_contexts */ public function test_get_contexts(): void { global $DB; $contexts = []; $contexts[] = \context_system::instance(); $contexts[] = \context_user::instance(\core_user::get_user_by_username('admin')->id); $ids = []; foreach ($contexts as $context) { $ids[] = $context->id; } $uit = new test_contextlist_base(); $uit->set_contextids($ids); $result = $uit->get_contexts(); $this->assertCount(count($contexts), $result); foreach ($contexts as $context) { $this->assertNotFalse(array_search($context, $result)); } } /** * Ensure that the contextlist_base is countable. * * @dataProvider get_contextids_provider * @param array $input List of context IDs * @param array $expected list of contextids * @param int $count Expected count * @covers ::count */ public function test_countable($input, $expected, $count): void { $uit = new test_contextlist_base(); $uit->set_contextids($input); $this->assertCount($count, $uit); } /** * Ensure that the contextlist_base iterates over the set of contexts. * * @covers ::current * @covers ::key * @covers ::next * @covers ::rewind * @covers ::valid */ public function test_context_iteration(): void { global $DB; $allcontexts = $DB->get_records('context'); $contexts = []; foreach ($allcontexts as $context) { $contexts[] = \context::instance_by_id($context->id); } $uit = new test_contextlist_base(); $uit->set_contextids(array_keys($allcontexts)); foreach ($uit as $key => $context) { $this->assertNotFalse(array_search($context, $contexts)); } } /** * Test that deleting a context results in current returning nothing. * * @covers ::current */ public function test_current_context_one_context(): void { global $DB; $this->resetAfterTest(); $data = (object) [ 'contextlevel' => CONTEXT_BLOCK, 'instanceid' => 45, 'path' => '1/5/67/107', 'depth' => 4 ]; $contextid = $DB->insert_record('context', $data); $contextbase = new test_contextlist_base(); $contextbase->set_contextids([$contextid]); $this->assertCount(1, $contextbase); $currentcontext = $contextbase->current(); $this->assertEquals($contextid, $currentcontext->id); $DB->delete_records('context', ['id' => $contextid]); context_helper::reset_caches(); $this->assertEmpty($contextbase->current()); } /** * Test that deleting a context results in the next record being returned. * * @covers ::current */ public function test_current_context_two_contexts(): void { global $DB; $this->resetAfterTest(); $data = (object) [ 'contextlevel' => CONTEXT_BLOCK, 'instanceid' => 45, 'path' => '1/5/67/107', 'depth' => 4 ]; $contextid1 = $DB->insert_record('context', $data); $data = (object) [ 'contextlevel' => CONTEXT_BLOCK, 'instanceid' => 47, 'path' => '1/5/54/213', 'depth' => 4 ]; $contextid2 = $DB->insert_record('context', $data); $contextbase = new test_contextlist_base(); $contextbase->set_contextids([$contextid1, $contextid2]); $this->assertCount(2, $contextbase); $DB->delete_records('context', ['id' => $contextid1]); context_helper::reset_caches(); // Current should return context 2. $this->assertEquals($contextid2, $contextbase->current()->id); } /** * Test that if there are no non-deleted contexts that nothing is returned. * * @covers ::get_contexts */ public function test_get_contexts_all_deleted(): void { global $DB; $this->resetAfterTest(); $data = (object) [ 'contextlevel' => CONTEXT_BLOCK, 'instanceid' => 45, 'path' => '1/5/67/107', 'depth' => 4 ]; $contextid = $DB->insert_record('context', $data); $contextbase = new test_contextlist_base(); $contextbase->set_contextids([$contextid]); $this->assertCount(1, $contextbase); $DB->delete_records('context', ['id' => $contextid]); context_helper::reset_caches(); $this->assertEmpty($contextbase->get_contexts()); } /** * Test that get_contexts() returns only active contexts. * * @covers ::get_contexts */ public function test_get_contexts_one_deleted(): void { global $DB; $this->resetAfterTest(); $data = (object) [ 'contextlevel' => CONTEXT_BLOCK, 'instanceid' => 45, 'path' => '1/5/67/107', 'depth' => 4 ]; $contextid1 = $DB->insert_record('context', $data); $data = (object) [ 'contextlevel' => CONTEXT_BLOCK, 'instanceid' => 47, 'path' => '1/5/54/213', 'depth' => 4 ]; $contextid2 = $DB->insert_record('context', $data); $contextbase = new test_contextlist_base(); $contextbase->set_contextids([$contextid1, $contextid2]); $this->assertCount(2, $contextbase); $DB->delete_records('context', ['id' => $contextid1]); context_helper::reset_caches(); $contexts = $contextbase->get_contexts(); $this->assertCount(1, $contexts); $context = array_shift($contexts); $this->assertEquals($contextid2, $context->id); } } /** * A test class extending the contextlist_base allowing setting of the * contextids. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class test_contextlist_base extends contextlist_base { /** * Set the contextids for the test class. * * @param int[] $contexids The list of contextids to use. */ public function set_contextids(array $contextids) { parent::set_contextids($contextids); } } approved_contextlist_test.php000064400000004253152265473400012610 0ustar00. /** * Unit Tests for the approved contextlist Class * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\local\request\approved_contextlist; /** * Tests for the \core_privacy API's approved contextlist functionality. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\approved_contextlist */ final class approved_contextlist_test extends advanced_testcase { /** * The approved contextlist should not be modifiable once set. * * @covers ::__construct * @covers \core_privacy\local\request\approved_contextlist */ public function test_default_values_set(): void { $testuser = \core_user::get_user_by_username('admin'); $contextids = [3, 2, 1]; $component = 'core_privacy'; $uit = new approved_contextlist($testuser, $component, $contextids); $this->assertEquals($testuser, $uit->get_user()); $this->assertEquals($component, $uit->get_component()); $result = $uit->get_contextids(); // Note: Array order is not guaranteed and should not matter. foreach ($contextids as $contextid) { $this->assertNotFalse(array_search($contextid, $result)); } } } userlist_test.php000064400000005732152265473400010205 0ustar00. /** * Unit Tests for the approved userlist Class * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\local\request\userlist; /** * Tests for the \core_privacy API's approved userlist functionality. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\userlist */ final class userlist_test extends advanced_testcase { /** * Ensure that valid SQL results in the relevant users being added. * * @covers ::add_from_sql */ public function test_add_from_sql(): void { global $DB; $sql = "SELECT c.id FROM {user} c"; $params = []; $allusers = $DB->get_records_sql($sql, $params); $uut = new userlist(\context_system::instance(), 'core_privacy'); $uut->add_from_sql('id', $sql, $params); $this->assertCount(count($allusers), $uut); } /** * Ensure that adding a single user adds that user. * * @covers ::add_user */ public function test_add_user(): void { $this->resetAfterTest(); $u1 = $this->getDataGenerator()->create_user(); $u2 = $this->getDataGenerator()->create_user(); $uut = new userlist(\context_system::instance(), 'core_privacy'); $uut->add_user($u1->id); $this->assertCount(1, $uut); $this->assertEquals($uut->current(), $u1); } /** * Ensure that adding multiple users by ID adds those users. * * @covers ::add_users */ public function test_add_users(): void { $this->resetAfterTest(); $u1 = $this->getDataGenerator()->create_user(); $u2 = $this->getDataGenerator()->create_user(); $u3 = $this->getDataGenerator()->create_user(); $expected = [$u1->id, $u3->id]; $uut = new userlist(\context_system::instance(), 'core_privacy'); $uut->add_users([$u1->id, $u3->id]); $this->assertCount(2, $uut); foreach ($uut as $user) { $this->assertNotFalse(array_search($user->id, $expected)); } } } sitepolicy_test.php000064400000036251152265473400010517 0ustar00. /** * Unit Tests for sitepolicy manager * * @package core_privacy * @category test * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; /** * Unit Tests for sitepolicy manager * * @package core_privacy * @category test * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class sitepolicy_test extends advanced_testcase { /** * Tests for \core_privacy\local\sitepolicy\manager::get_handler_classname() behaviour. */ public function test_get_handler_classname(): void { global $CFG; $this->resetAfterTest(true); $manager = $this->get_mock_manager_with_handler(); // If no handler is specified, then we should get the default one. $CFG->sitepolicyhandler = ''; $this->assertEquals($manager->get_handler_classname(), \core_privacy\local\sitepolicy\default_handler::class); // If non-existing handler is specified, we should get the default one too. $CFG->sitepolicyhandler = 'non_existing_plugin_which_i_really_hope_will_never_exist'; $this->assertEquals($manager->get_handler_classname(), \core_privacy\local\sitepolicy\default_handler::class); // If the defined handler is among known handlers, we should get its class name. $CFG->sitepolicyhandler = 'testtool_testhandler'; $this->assertEquals($manager->get_handler_classname(), 'mock_sitepolicy_handler'); } /** * Tests for \core_privacy\local\sitepolicy\manager::is_defined() */ public function test_is_defined(): void { global $CFG; $this->resetAfterTest(true); $manager = new \core_privacy\local\sitepolicy\manager(); $this->assertFalse($manager->is_defined(true)); $this->assertFalse($manager->is_defined(false)); $CFG->sitepolicy = 'http://example.com/sitepolicy.html'; $this->assertFalse($manager->is_defined(true)); $this->assertTrue($manager->is_defined(false)); $CFG->sitepolicyguest = 'http://example.com/sitepolicyguest.html'; $this->assertTrue($manager->is_defined(true)); $this->assertTrue($manager->is_defined(false)); $CFG->sitepolicy = null; $this->assertTrue($manager->is_defined(true)); $this->assertFalse($manager->is_defined(false)); // When non existing plugin is set as $CFG->sitepolicyhandler, assume that $CFG->sitepolicy* are all not set. $CFG->sitepolicy = 'http://example.com/sitepolicy.html'; $CFG->sitepolicyguest = 'http://example.com/sitepolicyguest.html'; $CFG->sitepolicyhandler = 'non_existing_plugin_which_i_really_hope_will_never_exist'; $this->assertFalse($manager->is_defined(true)); $this->assertFalse($manager->is_defined(false)); } /** * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() */ public function test_get_redirect_url(): void { global $CFG; $this->resetAfterTest(true); $manager = new \core_privacy\local\sitepolicy\manager(); $this->assertEquals(null, $manager->get_redirect_url(true)); $this->assertEquals(null, $manager->get_redirect_url(false)); $CFG->sitepolicy = 'http://example.com/sitepolicy.html'; $this->assertEquals(null, $manager->get_redirect_url(true)); $this->assertEquals($CFG->wwwroot.'/user/policy.php', $manager->get_redirect_url(false)->out(false)); $CFG->sitepolicyguest = 'http://example.com/sitepolicyguest.html'; $this->assertEquals($CFG->wwwroot.'/user/policy.php', $manager->get_redirect_url(true)->out(false)); $this->assertEquals($CFG->wwwroot.'/user/policy.php', $manager->get_redirect_url(false)->out(false)); $CFG->sitepolicy = null; $this->assertEquals($CFG->wwwroot.'/user/policy.php', $manager->get_redirect_url(true)->out(false)); $this->assertEquals(null, $manager->get_redirect_url(false)); // When non existing plugin is set as $CFG->sitepolicyhandler, assume that $CFG->sitepolicy* are all not set. $CFG->sitepolicy = 'http://example.com/sitepolicy.html'; $CFG->sitepolicyguest = 'http://example.com/sitepolicyguest.html'; $CFG->sitepolicyhandler = 'non_existing_plugin_which_i_really_hope_will_never_exist'; $this->assertEquals(null, $manager->get_redirect_url(true)); $this->assertEquals(null, $manager->get_redirect_url(false)); } /** * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() */ public function test_get_embed_url(): void { global $CFG; $this->resetAfterTest(true); $manager = new \core_privacy\local\sitepolicy\manager(); $this->assertEquals(null, $manager->get_embed_url(true)); $this->assertEquals(null, $manager->get_embed_url(false)); $CFG->sitepolicy = 'http://example.com/sitepolicy.html'; $this->assertEquals(null, $manager->get_embed_url(true)); $this->assertEquals($CFG->sitepolicy, $manager->get_embed_url(false)->out(false)); $CFG->sitepolicyguest = 'http://example.com/sitepolicyguest.html'; $this->assertEquals($CFG->sitepolicyguest, $manager->get_embed_url(true)->out(false)); $this->assertEquals($CFG->sitepolicy, $manager->get_embed_url(false)->out(false)); $CFG->sitepolicy = null; $this->assertEquals($CFG->sitepolicyguest, $manager->get_embed_url(true)->out(false)); $this->assertEquals(null, $manager->get_embed_url(false)); // When non existing plugin is set as $CFG->sitepolicyhandler, assume that $CFG->sitepolicy* are all not set. $CFG->sitepolicy = 'http://example.com/sitepolicy.html'; $CFG->sitepolicyguest = 'http://example.com/sitepolicyguest.html'; $CFG->sitepolicyhandler = 'non_existing_plugin_which_i_really_hope_will_never_exist'; $this->assertEquals(null, $manager->get_embed_url(true)); $this->assertEquals(null, $manager->get_embed_url(false)); } /** * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() */ public function test_accept(): void { global $CFG, $USER, $DB; $this->resetAfterTest(true); $manager = new \core_privacy\local\sitepolicy\manager(); // No site policy. $user1 = $this->getDataGenerator()->create_user(); $this->setUser($user1); $this->assertFalse($manager->accept()); $this->assertEquals(0, $USER->policyagreed); // With site policy. $CFG->sitepolicy = 'http://example.com/sitepolicy.html'; $user2 = $this->getDataGenerator()->create_user(); $this->setUser($user2); $this->assertEquals(0, $USER->policyagreed); $this->assertEquals(0, $DB->get_field('user', 'policyagreed', ['id' => $USER->id])); $this->assertTrue($manager->accept()); $this->assertEquals(1, $USER->policyagreed); $this->assertEquals(1, $DB->get_field('user', 'policyagreed', ['id' => $USER->id])); // When non existing plugin is set as $CFG->sitepolicyhandler, assume that $CFG->sitepolicy* are all not set. $CFG->sitepolicy = 'http://example.com/sitepolicy.html'; $CFG->sitepolicyhandler = 'non_existing_plugin_which_i_really_hope_will_never_exist'; $user3 = $this->getDataGenerator()->create_user(); $this->setUser($user3); $this->assertEquals(0, $USER->policyagreed); $this->assertFalse($manager->accept()); $this->assertEquals(0, $USER->policyagreed); } /** * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() for guests */ public function test_accept_guests(): void { global $CFG, $USER, $DB; $this->resetAfterTest(true); $manager = new \core_privacy\local\sitepolicy\manager(); $this->setGuestUser(); // No site policy. $this->assertFalse($manager->accept()); $this->assertEquals(0, $USER->policyagreed); // With site policy. $CFG->sitepolicyguest = 'http://example.com/sitepolicy.html'; $this->assertEquals(0, $USER->policyagreed); $this->assertTrue($manager->accept()); $this->assertEquals(1, $USER->policyagreed); $this->assertEquals(0, $DB->get_field('user', 'policyagreed', ['id' => $USER->id])); // When non existing plugin is set as $CFG->sitepolicyhandler, assume that $CFG->sitepolicy* are all not set. $USER->policyagreed = 0; // Reset. $CFG->sitepolicyguest = 'http://example.com/sitepolicyguest.html'; $CFG->sitepolicyhandler = 'non_existing_plugin_which_i_really_hope_will_never_exist'; $this->assertFalse($manager->accept()); $this->assertEquals(0, $USER->policyagreed); } /** * Helper to spoof the results of the internal function get_all_handlers, allowing mock handler to be tested. * * @return PHPUnit_Framework_MockObject_MockObject */ protected function get_mock_manager_with_handler() { global $CFG; require_once($CFG->dirroot.'/privacy/tests/fixtures/mock_sitepolicy_handler.php'); $mock = $this->getMockBuilder(\core_privacy\local\sitepolicy\manager::class) ->onlyMethods(['get_all_handlers']) ->getMock(); $mock->expects($this->any()) ->method('get_all_handlers') ->will($this->returnValue(['testtool_testhandler' => 'mock_sitepolicy_handler'])); return $mock; } /** * Tests for \core_privacy\local\sitepolicy\manager::is_defined() with a handler */ public function test_is_defined_with_handler(): void { global $CFG; $this->resetAfterTest(true); $CFG->sitepolicyhandler = 'testtool_testhandler'; $manager = $this->get_mock_manager_with_handler(); $this->assertTrue($manager->is_defined(true)); $this->assertTrue($manager->is_defined(false)); } /** * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() with a handler */ public function test_get_redirect_url_with_handler(): void { global $CFG; $this->resetAfterTest(true); $CFG->sitepolicyhandler = 'testtool_testhandler'; $manager = $this->get_mock_manager_with_handler(); $this->assertEquals('http://example.com/policy.php', $manager->get_redirect_url(true)->out(false)); $this->assertEquals('http://example.com/policy.php', $manager->get_redirect_url(false)->out(false)); } /** * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() with a handler */ public function test_get_embed_url_with_handler(): void { global $CFG; $this->resetAfterTest(true); $CFG->sitepolicyhandler = 'testtool_testhandler'; $manager = $this->get_mock_manager_with_handler(); $this->assertEquals('http://example.com/view.htm', $manager->get_embed_url(true)->out(false)); $this->assertEquals('http://example.com/view.htm', $manager->get_embed_url(false)->out(false)); } /** * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() with a handler */ public function test_accept_with_handler(): void { global $CFG, $USER, $DB; $this->resetAfterTest(true); $CFG->sitepolicyhandler = 'testtool_testhandler'; $manager = $this->get_mock_manager_with_handler(); $user2 = $this->getDataGenerator()->create_user(); $this->setUser($user2); $this->assertEquals(0, $USER->policyagreed); $this->assertEquals(0, $DB->get_field('user', 'policyagreed', ['id' => $USER->id])); $this->assertTrue($manager->accept()); $this->assertEquals(2, $USER->policyagreed); $this->assertEquals(2, $DB->get_field('user', 'policyagreed', ['id' => $USER->id])); } /** * Tests for \core_privacy\local\sitepolicy\manager::get_redirect_url() for guests with a handler */ public function test_accept_guests_with_handler(): void { global $CFG, $USER, $DB; $this->resetAfterTest(true); $CFG->sitepolicyhandler = 'testtool_testhandler'; $manager = $this->get_mock_manager_with_handler(); $this->setGuestUser(); $this->assertEquals(0, $USER->policyagreed); $this->assertTrue($manager->accept()); $this->assertEquals(2, $USER->policyagreed); $this->assertEquals(0, $DB->get_field('user', 'policyagreed', ['id' => $USER->id])); } } /** * Mock handler for site policies * * @package core_privacy * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class handler extends \core_privacy\local\sitepolicy\handler { /** * Checks if the site has site policy defined * * @param bool $forguests * @return bool */ public static function is_defined($forguests = false) { return true; } /** * Returns URL to redirect user to when user needs to agree to site policy * * This is a regular interactive page for web users. It should have normal Moodle header/footers, it should * allow user to view policies and accept them. * * @param bool $forguests * @return moodle_url|null (returns null if site policy is not defined) */ public static function get_redirect_url($forguests = false) { return 'http://example.com/policy.php'; } /** * Returns URL of the site policy that needs to be displayed to the user (inside iframe or to use in WS such as mobile app) * * This page should not have any header/footer, it does not also have any buttons/checkboxes. The caller needs to implement * the "Accept" button and call {@link self::accept()} on completion. * * @param bool $forguests * @return moodle_url|null */ public static function get_embed_url($forguests = false) { return 'http://example.com/view.htm'; } /** * Accept site policy for the current user * * @return bool - false if sitepolicy not defined, user is not logged in or user has already agreed to site policy; * true - if we have successfully marked the user as agreed to the site policy */ public static function accept() { global $USER, $DB; // Accepts policy on behalf of the current user. We set it to 2 here to check that this callback was called. $USER->policyagreed = 2; if (!isguestuser()) { $DB->update_record('user', ['policyagreed' => 2, 'id' => $USER->id]); } return true; } } tests_content_writer_test.php000064400000052403152265473400012620 0ustar00. /** * Unit Tests for the Content Writer used for unit testing. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\local\request\writer; use \core_privacy\tests\request\content_writer; /** * Unit Tests for the Content Writer used for unit testing. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class tests_content_writer_test extends advanced_testcase { /** * It should be possible to store and retrieve data. */ public function test_export_data(): void { $context = \context_system::instance(); $writer = $this->get_writer_instance(); $dataa = (object) [ 'example' => 'a', ]; $datab = (object) [ 'example' => 'b', ]; $writer->set_context($context) ->export_data(['data'], $dataa) ->export_data([], $datab); $data = $writer->get_data([]); $this->assertSame($datab, $data); $data = $writer->get_data(['data']); $this->assertSame($dataa, $data); } /** * It should be possible to store and retrieve data at the same point in different contexts. */ public function test_export_data_no_context_clash(): void { $writer = $this->get_writer_instance(); $context = \context_system::instance(); $dataa = (object) [ 'example' => 'a', ]; $writer->set_context($context) ->export_data(['data'], $dataa); $adminuser = \core_user::get_user_by_username('admin'); $usercontext = \context_user::instance($adminuser->id); $datab = (object) [ 'example' => 'b', ]; $writer->set_context($usercontext) ->export_data(['data'], $datab); $writer->set_context($context); $data = $writer->get_data(['data']); $this->assertSame($dataa, $data); $this->assertTrue($writer->has_any_data()); $this->assertTrue($writer->has_any_data(['data'])); $this->assertFalse($writer->has_any_data(['somepath'])); $writer->set_context($usercontext); $data = $writer->get_data(['data']); $this->assertSame($datab, $data); } /** * Test export and recover with children. */ public function test_get_data_with_children(): void { $writer = $this->get_writer_instance(); $context = \context_system::instance(); $writer->set_context($context) ->export_data(['a'], (object) ['parent' => true]) ->export_data(['a', 'b'], (object) ['parent' => false]); $this->assertTrue($writer->get_data(['a'])->parent); $this->assertFalse($writer->get_data(['a', 'b'])->parent); $this->assertEquals([], $writer->get_data(['a', 'b', 'c'])); } /** * It should be possible to store and retrieve metadata. */ public function test_export_metadata(): void { $context = \context_system::instance(); $writer = $this->get_writer_instance(); $writer->set_context($context) ->export_metadata(['metadata'], 'somekey', 'value1', 'description1') ->export_metadata([], 'somekey', 'value2', 'description2'); $allmetadata = $writer->get_all_metadata([]); $this->assertCount(1, $allmetadata); $this->assertArrayHasKey('somekey', $allmetadata); $this->assertEquals('value2', $allmetadata['somekey']->value); $this->assertEquals('description2', $allmetadata['somekey']->description); $metadata = $writer->get_metadata([], 'somekey', false); $this->assertEquals('value2', $metadata->value); $this->assertEquals('description2', $metadata->description); $this->assertEquals('value2', $writer->get_metadata([], 'somekey', true)); $allmetadata = $writer->get_all_metadata(['metadata']); $this->assertCount(1, $allmetadata); $this->assertArrayHasKey('somekey', $allmetadata); $this->assertEquals('value1', $allmetadata['somekey']->value); $this->assertEquals('description1', $allmetadata['somekey']->description); $metadata = $writer->get_metadata(['metadata'], 'somekey', false); $this->assertEquals('value1', $metadata->value); $this->assertEquals('description1', $metadata->description); $this->assertEquals('value1', $writer->get_metadata(['metadata'], 'somekey', true)); } /** * It should be possible to store and retrieve metadata at the same point in different contexts. */ public function test_export_metadata_no_context_clash(): void { $writer = $this->get_writer_instance(); $context = \context_system::instance(); $writer->set_context($context) ->export_metadata(['metadata'], 'somekey', 'value1', 'description1'); $adminuser = \core_user::get_user_by_username('admin'); $usercontext = \context_user::instance($adminuser->id); $writer->set_context($usercontext) ->export_metadata(['metadata'], 'somekey', 'value2', 'description2'); $writer->set_context($context); $allmetadata = $writer->get_all_metadata(['metadata']); $this->assertCount(1, $allmetadata); $this->assertArrayHasKey('somekey', $allmetadata); $this->assertEquals('value1', $allmetadata['somekey']->value); $this->assertEquals('description1', $allmetadata['somekey']->description); $metadata = $writer->get_metadata(['metadata'], 'somekey', false); $this->assertEquals('value1', $metadata->value); $this->assertEquals('description1', $metadata->description); $this->assertEquals('value1', $writer->get_metadata(['metadata'], 'somekey', true)); $writer->set_context($usercontext); $allmetadata = $writer->get_all_metadata(['metadata']); $this->assertCount(1, $allmetadata); $this->assertArrayHasKey('somekey', $allmetadata); $this->assertEquals('value2', $allmetadata['somekey']->value); $this->assertEquals('description2', $allmetadata['somekey']->description); $metadata = $writer->get_metadata(['metadata'], 'somekey', false); $this->assertEquals('value2', $metadata->value); $this->assertEquals('description2', $metadata->description); $this->assertEquals('value2', $writer->get_metadata(['metadata'], 'somekey', true)); $this->assertTrue($writer->has_any_data()); $this->assertTrue($writer->has_any_data(['metadata'])); $this->assertFalse($writer->has_any_data(['somepath'])); } /** * It should be possible to store and retrieve user preferences. */ public function test_export_user_preference(): void { $context = \context_system::instance(); $adminuser = \core_user::get_user_by_username('admin'); $usercontext = \context_user::instance($adminuser->id); $writer = $this->get_writer_instance(); $writer->set_context($context) ->export_user_preference('core_privacy', 'somekey', 'value0', 'description0'); $writer->set_context($usercontext) ->export_user_preference('core_tests', 'somekey', 'value1', 'description1') ->export_user_preference('core_privacy', 'somekey', 'value2', 'description2') ->export_user_preference('core_tests', 'someotherkey', 'value2', 'description2'); $writer->set_context($usercontext); $someprefs = $writer->get_user_preferences('core_privacy'); $this->assertCount(1, (array) $someprefs); $this->assertTrue(isset($someprefs->somekey)); $this->assertEquals('value0', $someprefs->somekey->value); $this->assertEquals('description0', $someprefs->somekey->description); $someprefs = $writer->get_user_context_preferences('core_tests'); $this->assertCount(2, (array) $someprefs); $this->assertTrue(isset($someprefs->somekey)); $this->assertEquals('value1', $someprefs->somekey->value); $this->assertEquals('description1', $someprefs->somekey->description); $this->assertTrue(isset($someprefs->someotherkey)); $this->assertEquals('value2', $someprefs->someotherkey->value); $this->assertEquals('description2', $someprefs->someotherkey->description); $someprefs = $writer->get_user_context_preferences('core_privacy'); $this->assertCount(1, (array) $someprefs); $this->assertTrue(isset($someprefs->somekey)); $this->assertEquals('value2', $someprefs->somekey->value); $this->assertEquals('description2', $someprefs->somekey->description); } /** * It should be possible to store and retrieve user preferences at the same point in different contexts. */ public function test_export_user_preference_no_context_clash(): void { $writer = $this->get_writer_instance(); $context = \context_system::instance(); $coursecontext = \context_course::instance(SITEID); $adminuser = \core_user::get_user_by_username('admin'); $usercontext = \context_user::instance($adminuser->id); $writer->set_context($context) ->export_user_preference('core_tests', 'somekey', 'value0', 'description0'); $writer->set_context($coursecontext) ->export_user_preference('core_tests', 'somekey', 'value1', 'description1'); $writer->set_context($usercontext) ->export_user_preference('core_tests', 'somekey', 'value2', 'description2'); // Set the course context and fetch with get_user_preferences to get the global preference. $writer->set_context($coursecontext); $someprefs = $writer->get_user_preferences('core_tests'); $this->assertCount(1, (array) $someprefs); $this->assertTrue(isset($someprefs->somekey)); $this->assertEquals('value0', $someprefs->somekey->value); $this->assertEquals('description0', $someprefs->somekey->description); // Set the course context and fetch with get_user_context_preferences. $someprefs = $writer->get_user_context_preferences('core_tests'); $this->assertCount(1, (array) $someprefs); $this->assertTrue(isset($someprefs->somekey)); $this->assertEquals('value1', $someprefs->somekey->value); $this->assertEquals('description1', $someprefs->somekey->description); $writer->set_context($usercontext); $someprefs = $writer->get_user_context_preferences('core_tests'); $this->assertCount(1, (array) $someprefs); $this->assertTrue(isset($someprefs->somekey)); $this->assertEquals('value2', $someprefs->somekey->value); $this->assertEquals('description2', $someprefs->somekey->description); } /** * Test export and recover with children. */ public function test_get_metadata_with_children(): void { $writer = $this->get_writer_instance(); $context = \context_system::instance(); $writer->set_context($context) ->export_metadata(['a'], 'abc', 'ABC', 'A, B, C') ->export_metadata(['a', 'b'], 'def', 'DEF', 'D, E, F'); $this->assertEquals('ABC', $writer->get_metadata(['a'], 'abc')); $this->assertEquals('DEF', $writer->get_metadata(['a', 'b'], 'def')); } /** * It should be possible to export files in the files and children contexts. */ public function test_export_file_special_folders(): void { $context = \context_system::instance(); $filea = $this->get_stored_file('/', 'files'); $fileb = $this->get_stored_file('/children/', 'foo.zip'); $writer = $this->get_writer_instance() ->set_context($context) ->export_file([], $filea) ->export_file([], $fileb); $files = $writer->get_files([]); $this->assertCount(2, $files); $this->assertEquals($filea, $files['files']); $this->assertEquals($fileb, $files['children/foo.zip']); } /** * It should be possible to export mutliple files in the same subcontext/path space but different context and not * have them clash. */ public function test_export_file_no_context_clash(): void { $writer = $this->get_writer_instance(); $context = \context_system::instance(); $filea = $this->get_stored_file('/foo/', 'foo.txt'); $writer = $this->get_writer_instance() ->set_context($context) ->export_file([], $filea); $adminuser = \core_user::get_user_by_username('admin'); $usercontext = \context_user::instance($adminuser->id); $fileb = $this->get_stored_file('/foo/', 'foo.txt'); $writer->set_context($usercontext) ->export_file([], $fileb); $writer->set_context($context); $files = $writer->get_files([]); $this->assertCount(1, $files); $this->assertEquals($filea, $files['foo/foo.txt']); $writer->set_context($usercontext); $files = $writer->get_files([]); $this->assertCount(1, $files); $this->assertEquals($fileb, $files['foo/foo.txt']); $this->assertTrue($writer->has_any_data()); $this->assertFalse($writer->has_any_data(['somepath'])); } /** * Test export and recover with children. */ public function test_get_file_with_children(): void { $writer = $this->get_writer_instance(); $context = \context_system::instance(); $filea = $this->get_stored_file('/foo/', 'foo.txt'); $fileb = $this->get_stored_file('/foo/', 'foo.txt'); $writer->set_context($context) ->export_file(['a'], $filea) ->export_file(['a', 'b'], $fileb); $files = $writer->get_files(['a']); $this->assertCount(1, $files); $this->assertEquals($filea, $files['foo/foo.txt']); $files = $writer->get_files(['a', 'b']); $this->assertCount(1, $files); $this->assertEquals($fileb, $files['foo/foo.txt']); } /** * It should be possible to export related data in the files and children contexts. */ public function test_export_related_data(): void { $context = \context_system::instance(); $writer = $this->get_writer_instance() ->set_context($context) ->export_related_data(['file', 'data'], 'file', 'data1') ->export_related_data([], 'file', 'data2'); $data = $writer->get_related_data([]); $this->assertCount(1, $data); $this->assertEquals('data2', $data['file']); $data = $writer->get_related_data([], 'file'); $this->assertEquals('data2', $data); $data = $writer->get_related_data(['file', 'data']); $this->assertCount(1, $data); $this->assertEquals('data1', $data['file']); $data = $writer->get_related_data(['file', 'data'], 'file'); $this->assertEquals('data1', $data); $this->assertTrue($writer->has_any_data()); $this->assertTrue($writer->has_any_data(['file'])); $this->assertTrue($writer->has_any_data(['file', 'data'])); $this->assertFalse($writer->has_any_data(['somepath'])); } /** * It should be possible to export related data in the same location,but in a different context. */ public function test_export_related_data_no_context_clash(): void { $writer = $this->get_writer_instance(); $context = \context_system::instance(); $writer->set_context($context) ->export_related_data(['file', 'data'], 'file', 'data1'); $adminuser = \core_user::get_user_by_username('admin'); $usercontext = \context_user::instance($adminuser->id); $writer->set_context($usercontext) ->export_related_data(['file', 'data'], 'file', 'data2'); $writer->set_context($context); $data = $writer->get_related_data(['file', 'data']); $this->assertCount(1, $data); $this->assertEquals('data1', $data['file']); $writer->set_context($usercontext); $data = $writer->get_related_data(['file', 'data']); $this->assertCount(1, $data); $this->assertEquals('data2', $data['file']); } /** * Test export and recover with children. */ public function test_get_related_data_with_children(): void { $writer = $this->get_writer_instance(); $context = \context_system::instance(); $writer->set_context($context) ->export_related_data(['a'], 'abc', 'ABC') ->export_related_data(['a', 'b'], 'def', 'DEF'); $this->assertEquals('ABC', $writer->get_related_data(['a'], 'abc')); $this->assertEquals('DEF', $writer->get_related_data(['a', 'b'], 'def')); } /** * It should be possible to export related files in the files and children contexts. */ public function test_export_custom_file(): void { $context = \context_system::instance(); $writer = $this->get_writer_instance() ->set_context($context) ->export_custom_file(['file.txt'], 'file.txt', 'Content 1') ->export_custom_file([], 'file.txt', 'Content 2'); $files = $writer->get_custom_file([]); $this->assertCount(1, $files); $this->assertEquals('Content 2', $files['file.txt']); $file = $writer->get_custom_file([], 'file.txt'); $this->assertEquals('Content 2', $file); $files = $writer->get_custom_file(['file.txt']); $this->assertCount(1, $files); $this->assertEquals('Content 1', $files['file.txt']); $file = $writer->get_custom_file(['file.txt'], 'file.txt'); $this->assertEquals('Content 1', $file); $this->assertTrue($writer->has_any_data()); $this->assertTrue($writer->has_any_data(['file.txt'])); $this->assertFalse($writer->has_any_data(['somepath'])); } /** * It should be possible to export related files in the same location * in different contexts. */ public function test_export_custom_file_no_context_clash(): void { $writer = $this->get_writer_instance(); $context = \context_system::instance(); $writer->set_context($context) ->export_custom_file(['file.txt'], 'file.txt', 'Content 1'); $adminuser = \core_user::get_user_by_username('admin'); $usercontext = \context_user::instance($adminuser->id); $writer->set_context($usercontext) ->export_custom_file(['file.txt'], 'file.txt', 'Content 2'); $writer->set_context($context); $files = $writer->get_custom_file(['file.txt']); $this->assertCount(1, $files); $this->assertEquals('Content 1', $files['file.txt']); $writer->set_context($usercontext); $files = $writer->get_custom_file(['file.txt']); $this->assertCount(1, $files); $this->assertEquals('Content 2', $files['file.txt']); } /** * Test export and recover with children. */ public function test_get_custom_file_with_children(): void { $writer = $this->get_writer_instance(); $context = \context_system::instance(); $writer->set_context($context) ->export_custom_file(['a'], 'file.txt', 'ABC') ->export_custom_file(['a', 'b'], 'file.txt', 'DEF'); $this->assertEquals('ABC', $writer->get_custom_file(['a'], 'file.txt')); $this->assertEquals('DEF', $writer->get_custom_file(['a', 'b'], 'file.txt')); } /** * Get a fresh content writer. * * @return moodle_content_writer */ public function get_writer_instance() { $factory = $this->createMock(writer::class); return new content_writer($factory); } /** * Helper to create a stored file objectw with the given supplied content. * * @param string $filepath The file path to use in the stored_file * @param string $filename The file name to use in the stored_file * @return stored_file */ protected function get_stored_file($filepath, $filename) { static $counter = 0; $counter++; $filecontent = "Example content {$counter}"; $contenthash = file_storage::hash_from_string($filecontent); $file = $this->getMockBuilder(stored_file::class) ->onlyMethods([]) ->setConstructorArgs([ get_file_storage(), (object) [ 'contenthash' => $contenthash, 'filesize' => strlen($filecontent), 'filepath' => $filepath, 'filename' => $filename, ] ]) ->getMock(); return $file; } } request_transform_test.php000064400000010171152265473400012107 0ustar00. /** * Unit Tests for the request transform helper. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\local\request\transform; /** * Tests for the \core_privacy API's request transform helper functionality. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\transform */ final class request_transform_test extends advanced_testcase { /** * Test that user translation currently does nothing. * * We have not determined if we will do this or not, but we provide the functionality and encourgae people to use * it so that it can be retrospectively fitted if required. * * @covers ::user */ public function test_user(): void { // Note: This test currently sucks, but there's no point creating users just to test this. for ($i = 0; $i < 10; $i++) { $this->assertEquals($i, transform::user($i)); } } /** * Test that the datetime is translated into a string. * * @covers ::datetime */ public function test_datetime(): void { $time = 1; $datestr = transform::datetime($time); // Assert it is a string. $this->assertIsString($datestr); // To prevent failures on MAC where we are returned with a lower-case 'am' we want to convert this to 'AM'. $datestr = str_replace('am', 'AM', $datestr); // Assert the formatted date is correct. $dateobj = new DateTime(); $dateobj->setTimestamp($time); $this->assertEquals($dateobj->format('l, j F Y, g:i A'), $datestr); } /** * Test that the date is translated into a string. * * @covers ::date */ public function test_date(): void { $time = 1; $datestr = transform::date($time); // Assert it is a string. $this->assertIsString($datestr); // Assert the formatted date is correct. $dateobj = new DateTime(); $dateobj->setTimestamp($time); $this->assertEquals($dateobj->format('j F Y'), $datestr); } /** * Ensure that the yesno function translates correctly. * * @dataProvider yesno_provider * @param mixed $input The input to test * @param string $expected The expected value * @covers ::yesno */ public function test_yesno($input, $expected): void { $this->assertEquals($expected, transform::yesno($input)); } /** * Data provider for tests of the yesno transformation. * * @return array */ public static function yesno_provider(): array { return [ 'Bool False' => [ false, get_string('no'), ], 'Bool true' => [ true, get_string('yes'), ], 'Int 0' => [ 0, get_string('no'), ], 'Int 1' => [ 1, get_string('yes'), ], 'String 0' => [ '0', get_string('no'), ], 'String 1' => [ '1', get_string('yes'), ], ]; } } types_external_location_test.php000064400000010641152265473400013264 0ustar00. namespace core_privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; use core_privacy\local\metadata\types\external_location; /** * Tests for the \core_privacy API's types\external_location functionality. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\metadata\types\external_location */ final class types_external_location_test extends \advanced_testcase { /** * Ensure that warnings are thrown if string identifiers contain invalid characters. * * @dataProvider invalid_string_provider * @param string $name Name * @param array $fields List of fields * @param string $summary Summary * @covers ::__construct */ public function test_invalid_configs($name, $fields, $summary): void { $record = new external_location($name, $fields, $summary); $this->assertDebuggingCalled(); } /** * Ensure that warnings are not thrown if debugging is not enabled, even if string identifiers contain invalid characters. * * @dataProvider invalid_string_provider * @param string $name Name * @param array $fields List of fields * @param string $summary Summary * @covers ::__construct */ public function test_invalid_configs_debug_normal($name, $fields, $summary): void { global $CFG; $this->resetAfterTest(); $CFG->debug = DEBUG_NORMAL; $record = new external_location($name, $fields, $summary); $this->assertDebuggingNotCalled(); } /** * Ensure that no warnings are shown for valid combinations. * * @dataProvider valid_string_provider * @param string $name Name * @param array $fields List of fields * @param string $summary Summary * @covers ::__construct */ public function test_valid_configs($name, $fields, $summary): void { $record = new external_location($name, $fields, $summary); $this->assertDebuggingNotCalled(); } /** * Data provider with a list of invalid string identifiers. * * @return array */ public static function invalid_string_provider(): array { return [ 'Space in summary' => [ 'example', [ 'field' => 'privacy:valid', ], 'This table is used for purposes.', ], 'Comma in summary' => [ 'example', [ 'field' => 'privacy:valid', ], 'privacy,foo', ], 'Space in field name' => [ 'example', [ 'field' => 'This field is used for purposes.', ], 'privacy:valid', ], 'Comma in field name' => [ 'example', [ 'field' => 'invalid,name', ], 'privacy:valid', ], 'No fields specified' => [ 'example', [], 'privacy:example:valid', ], ]; } /** * Data provider with a list of valid string identifiers. * * @return array */ public static function valid_string_provider(): array { return [ 'Valid combination' => [ 'example', [ 'field' => 'privacy:example:valid:field', 'field2' => 'privacy:example:valid:field2', ], 'privacy:example:valid', ], ]; } } types_database_table_test.php000064400000010614152265473400012465 0ustar00. namespace core_privacy; use core_privacy\local\metadata\types\database_table; defined('MOODLE_INTERNAL') || die(); global $CFG; /** * Tests for the \core_privacy API's types\database_table functionality. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\metadata\types\database_table */ final class types_database_table_test extends \advanced_testcase { /** * Ensure that warnings are thrown if string identifiers contain invalid characters. * * @dataProvider invalid_string_provider * @param string $name Name * @param array $fields List of fields * @param string $summary Summary * @covers ::__construct */ public function test_invalid_configs($name, $fields, $summary): void { $record = new database_table($name, $fields, $summary); $this->assertDebuggingCalled(); } /** * Ensure that warnings are not thrown if debugging is not enabled, even if string identifiers contain invalid characters. * * @dataProvider invalid_string_provider * @param string $name Name * @param array $fields List of fields * @param string $summary Summary * @covers ::__construct */ public function test_invalid_configs_debug_normal($name, $fields, $summary): void { global $CFG; $this->resetAfterTest(); $CFG->debug = DEBUG_NORMAL; $record = new database_table($name, $fields, $summary); $this->assertDebuggingNotCalled(); } /** * Ensure that no warnings are shown for valid combinations. * * @dataProvider valid_string_provider * @param string $name Name * @param array $fields List of fields * @param string $summary Summary * @covers ::__construct */ public function test_valid_configs($name, $fields, $summary): void { $record = new database_table($name, $fields, $summary); $this->assertDebuggingNotCalled(); } /** * Data provider with a list of invalid string identifiers. * * @return array */ public static function invalid_string_provider(): array { return [ 'Space in summary' => [ 'example', [ 'field' => 'privacy:valid', ], 'This table is used for purposes.', ], 'Comma in summary' => [ 'example', [ 'field' => 'privacy:valid', ], 'privacy,foo', ], 'Space in field name' => [ 'example', [ 'field' => 'This field is used for purposes.', ], 'privacy:valid', ], 'Comma in field name' => [ 'example', [ 'field' => 'invalid,name', ], 'privacy:valid', ], 'No fields specified' => [ 'example', [], 'privacy:example:valid', ], ]; } /** * Data provider with a list of valid string identifiers. * * @return array */ public static function valid_string_provider(): array { return [ 'Valid combination' => [ 'example', [ 'field' => 'privacy:example:valid:field', 'field2' => 'privacy:example:valid:field2', ], 'privacy:example:valid', ], ]; } } contextlist_test.php000064400000015241152265473400010707 0ustar00. /** * Unit Tests for the approved contextlist Class * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\local\request\contextlist; /** * Tests for the \core_privacy API's approved contextlist functionality. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\contextlist */ final class contextlist_test extends advanced_testcase { /** * Ensure that valid SQL results in the relevant contexts being added. * * @covers ::add_from_sql */ public function test_add_from_sql(): void { global $DB; $sql = "SELECT c.id FROM {context} c"; $params = []; $allcontexts = $DB->get_records_sql($sql, $params); $uit = new contextlist(); $uit->add_from_sql($sql, $params); $this->assertCount(count($allcontexts), $uit); } /** * Ensure that valid system context id is added. * * @covers ::add_system_context */ public function test_add_system_context(): void { $cl = new contextlist(); $cl->add_system_context(); $this->assertCount(1, $cl); foreach ($cl->get_contexts() as $context) { $this->assertEquals(SYSCONTEXTID, $context->id); } } /** * Ensure that a valid user context id is added. * * @covers ::add_user_context */ public function test_add_user_context(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->create_user(); $cl = new contextlist(); $cl->add_user_context($user->id); $this->assertCount(1, $cl); foreach ($cl->get_contexts() as $context) { $this->assertEquals(\context_user::instance($user->id)->id, $context->id); } } /** * Ensure that valid user contexts are added. * * @covers ::add_user_contexts */ public function test_add_user_contexts(): void { $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->create_user(); $cl = new contextlist(); $cl->add_user_contexts([$user1->id, $user2->id]); $this->assertCount(2, $cl); $contexts = $cl->get_contextids(); $this->assertContainsEquals(\context_user::instance($user1->id)->id, $contexts); $this->assertContainsEquals(\context_user::instance($user2->id)->id, $contexts); } /** * Test {@link \core_privacy\local\request\contextlist::test_guess_id_field_from_sql()} implementation. * * @dataProvider data_guess_id_field_from_sql * @param string $sql Input SQL we try to extract the context id field name from. * @param string $expected Expected detected value. * @covers ::guess_id_field_from_sql */ public function test_guess_id_field_from_sql($sql, $expected): void { $rc = new \ReflectionClass(contextlist::class); $rcm = $rc->getMethod('guess_id_field_from_sql'); $actual = $rcm->invoke(new contextlist(), $sql); $this->assertEquals($expected, $actual, 'Unable to guess context id field in: '.$sql); } /** * Provides data sets for {@link self::test_guess_id_field_from_sql()}. * * @return array */ public static function data_guess_id_field_from_sql(): array { return [ 'easy' => [ 'SELECT contextid FROM {foo}', 'contextid', ], 'with_distinct' => [ 'SELECT DISTINCT contextid FROM {foo}', 'contextid', ], 'with_dot' => [ 'SELECT cx.id FROM {foo} JOIN {context} cx ON blahblahblah', 'id', ], 'letter_case_does_not_matter' => [ 'Select ctxid From {foo} Where bar = ?', 'ctxid', ], 'alias' => [ 'SELECT foo.contextid AS ctx FROM {bar} JOIN {foo} ON bar.id = foo.barid', 'ctx', ], 'tabs' => [ "SELECT\tctxid\t\tFROM foo f", 'ctxid', ], 'whitespace' => [ "SELECT ctxid\t \tFROM foo f", 'ctxid', ], 'just_number' => [ '1', '1', ], 'select_number' => [ 'SELECT 2', '2', ], 'select_number_with_semicolon' => [ 'SELECT 3;', '3', ], 'select_number_from_table' => [ 'SELECT 4 FROM users', '4', ], 'select_with_complex_subqueries' => [ 'SELECT id FROM table WHERE id IN ( SELECT x FROM xtable UNION SELECT y FROM ( SELECT y FROM ytable JOIN ztable ON (z = y)))', 'id' ], 'invalid_union_with_first_being_column_name' => [ 'SELECT id FROM table UNION SELECT 1 FROM table', '' ], 'invalid_union_with_first_being_numeric' => [ 'SELECT 1 FROM table UNION SELECT id FROM table', '' ], 'invalid_union_without_from' => [ 'SELECT 1 UNION SELECT id FROM table', '' ], 'invalid_1' => [ 'SELECT 1+1', '', ], 'invalid_2' => [ 'muhehe', '', ], ]; } } request_helper_test.php000064400000017565152265473400011371 0ustar00. /** * Unit Tests for the request helper. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\local\request\helper; use \core_privacy\local\request\writer; /** * Tests for the \core_privacy API's request helper functionality. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\helper */ final class request_helper_test extends advanced_testcase { /** * Test that basic module data is returned. * * @covers ::get_context_data */ public function test_get_context_data_context_module(): void { $this->resetAfterTest(); // Setup. $course = $this->getDataGenerator()->create_course(); $user = \core_user::get_user_by_username('admin'); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); $context = context_module::instance($forum->cmid); $modinfo = get_fast_modinfo($course->id); $cm = $modinfo->cms[$context->instanceid]; // Fetch the data. $result = helper::get_context_data($context, $user); $this->assertInstanceOf('stdClass', $result); // Check that the name matches. $this->assertEquals($cm->get_formatted_name(), $result->name); // This plugin supports the intro. Check that it is included and correct. $formattedintro = format_text($forum->intro, $forum->introformat, [ 'noclean' => true, 'para' => false, 'context' => $context, 'overflowdiv' => true, ]); $this->assertEquals($formattedintro, $result->intro); // This function should only fetch data. It does not export it. $this->assertFalse(writer::with_context($context)->has_any_data()); } /** * Test that basic block data is returned. * * @covers ::get_context_data */ public function test_get_context_data_context_block(): void { $this->resetAfterTest(); // Setup. $block = $this->getDataGenerator()->create_block('online_users'); $context = context_block::instance($block->id); $user = \core_user::get_user_by_username('admin'); // Fetch the data. $data = helper::get_context_data($context, $user); $this->assertEquals(get_string('pluginname', 'block_online_users'), $data->blocktype); // This function should only fetch data. It does not export it. $this->assertFalse(writer::with_context($context)->has_any_data()); } /** * Test that a course moudle with completion tracking enabled has the completion data returned. * * @covers ::get_context_data */ public function test_get_context_data_context_module_completion(): void { $this->resetAfterTest(); // Create a module and set completion. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id, 'completion' => 1]); $context = context_module::instance($assign->cmid); $cm = get_coursemodule_from_id('assign', $assign->cmid); // Fetch context data. $contextdata = helper::get_context_data($context, $user); // Completion state is zero. // Check non completion for a user. $this->assertEquals(0, $contextdata->completion->state); // Complete the activity as a user. $completioninfo = new completion_info($course); $completioninfo->update_state($cm, COMPLETION_COMPLETE, $user->id); // Check that completion is now exported. $contextdata = helper::get_context_data($context, $user); $this->assertEquals(1, $contextdata->completion->state); // This function should only fetch data. It does not export it. $this->assertFalse(writer::with_context($context)->has_any_data()); } /** * Test that when there are no files to export for a course module context, nothing is exported. * * @covers ::export_context_files */ public function test_export_context_files_context_module_no_files(): void { $this->resetAfterTest(); // Setup. $course = $this->getDataGenerator()->create_course(); $user = \core_user::get_user_by_username('admin'); $forum = $this->getDataGenerator()->create_module('forum', [ 'course' => $course->id, ]); $context = context_module::instance($forum->cmid); $modinfo = get_fast_modinfo($course->id); $cm = $modinfo->cms[$context->instanceid]; // Fetch the data. helper::export_context_files($context, $user); // This function should only fetch data. It does not export it. $this->assertFalse(writer::with_context($context)->has_any_data()); } /** * Test that when there are no files to export for a course context, nothing is exported. * * @covers ::export_context_files */ public function test_export_context_files_context_course_no_files(): void { $this->resetAfterTest(); // Setup. $course = $this->getDataGenerator()->create_course(); $user = \core_user::get_user_by_username('admin'); $context = context_course::instance($course->id); // Fetch the data. helper::export_context_files($context, $user); // This function should only fetch data. It does not export it. $this->assertFalse(writer::with_context($context)->has_any_data()); } /** * Test that when there are files to export for a course context, the files are exported. * * @covers ::export_context_files */ public function test_export_context_files_context_course_intro_files(): void { $this->resetAfterTest(); // Setup. $course = $this->getDataGenerator()->create_course(); $user = \core_user::get_user_by_username('admin'); $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]); $context = context_module::instance($assign->cmid); // File details. $filerecord = array( 'contextid' => $context->id, 'component' => 'mod_assign', 'filearea' => 'intro', 'itemid' => 0, 'filepath' => '/', 'filename' => 'logo.png', ); $content = file_get_contents(self::get_fixture_path('core_privacy', 'logo.png')); // Store the file. $fs = get_file_storage(); $file = $fs->create_file_from_string($filerecord, $content); // Fetch the data. helper::export_context_files($context, $user); // This should have resulted in the file being exported. $this->assertTrue(writer::with_context($context)->has_any_data()); } } writer_test.php000064400000005404152265473400007643 0ustar00. namespace dataformat_ods; use core\dataformat; /** * Tests for the dataformat_ods writer * * @package dataformat_ods * @copyright 2022 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class writer_test extends \advanced_testcase { /** * Test writing data whose content contains an image with pluginfile.php source */ public function test_write_data(): void { $columns = ['fruit', 'colour', 'animal']; $rows = [ ['banana', 'yellow', 'monkey'], ['apple', 'red', 'wolf'], ['melon', 'green', 'aardvark'], ]; // Export to file. $exportfile = dataformat::write_data('My export', 'ods', $columns, $rows); // Read the file. $odscells = $this->get_ods_rows_content(file_get_contents($exportfile)); $this->assertEquals(array_merge([$columns], $rows), $odscells); } /** * Get ods rows from binary content * @param string $content * @return array * @throws \OpenSpout\Common\Exception\IOException * @throws \OpenSpout\Reader\Exception\ReaderNotOpenedException */ private function get_ods_rows_content($content) { $file = tempnam(sys_get_temp_dir(), 'ods_'); $handle = fopen($file, "w"); fwrite($handle, $content); /** @var \OpenSpout\Reader\ODS\Reader $reader */ $reader = \OpenSpout\Reader\Common\Creator\ReaderFactory::createFromFileByMimeType($file); $reader->open($file); /** @var \OpenSpout\Reader\ODS\Sheet[] $sheets */ $sheets = $reader->getSheetIterator(); $rowscellsvalues = []; foreach ($sheets as $sheet) { /** @var \OpenSpout\Common\Entity\Row[] $rows */ $rows = $sheet->getRowIterator(); foreach ($rows as $row) { $thisvalues = []; foreach ($row->getCells() as $cell) { $thisvalues[] = $cell->getValue(); } $rowscellsvalues[] = $thisvalues; } } return $rowscellsvalues; } } userlist_base_test.php000064400000017450152265473400011177 0ustar00. /** * Unit Tests for the abstract userlist Class * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\local\request\userlist_base; /** * Tests for the \core_privacy API's userlist base functionality. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\userlist_base */ final class userlist_base_test extends advanced_testcase { /** * Ensure that get_userids returns the list of unique userids. * * @dataProvider get_userids_provider * @param array $input List of user IDs * @param array $expected list of userids * @param int $count Expected count * @covers ::get_userids */ public function test_get_userids($input, $expected, $count): void { $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); $uut->set_userids($input); $result = $uut->get_userids(); $this->assertCount($count, $result); // Note: Array order is not guaranteed and should not matter. foreach ($expected as $userid) { $this->assertNotFalse(array_search($userid, $result)); } } /** * Provider for the list of userids. * * @return array */ public static function get_userids_provider(): array { return [ 'basic' => [ [1, 2, 3, 4, 5], [1, 2, 3, 4, 5], 5, ], 'duplicates' => [ [1, 1, 2, 2, 3, 4, 5], [1, 2, 3, 4, 5], 5, ], 'Mixed order with duplicates' => [ [5, 4, 2, 5, 4, 1, 3, 4, 1, 5, 5, 5, 2, 4, 1, 2], [1, 2, 3, 4, 5], 5, ], ]; } /** * Ensure that get_users returns the correct list of users. * * @covers ::get_users */ public function test_get_users(): void { $this->resetAfterTest(); $users = []; $user = $this->getDataGenerator()->create_user(); $users[$user->id] = $user; $user = $this->getDataGenerator()->create_user(); $users[$user->id] = $user; $user = $this->getDataGenerator()->create_user(); $users[$user->id] = $user; $otheruser = $this->getDataGenerator()->create_user(); $ids = array_keys($users); $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); $uut->set_userids($ids); $result = $uut->get_users(); sort($users); sort($result); $this->assertCount(3, $result); $this->assertEquals($users, $result); } /** * Ensure that the userlist_base is countable. * * @dataProvider get_userids_provider * @param array $input List of user IDs * @param array $expected list of userids * @param int $count Expected count * @covers ::count */ public function test_countable($input, $expected, $count): void { $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); $uut->set_userids($input); $this->assertCount($count, $uut); } /** * Ensure that the userlist_base iterates over the set of users. * * @covers ::current * @covers ::key * @covers ::next * @covers ::rewind * @covers ::valid */ public function test_user_iteration(): void { $this->resetAfterTest(); $users = []; $user = $this->getDataGenerator()->create_user(); $users[$user->id] = $user; $user = $this->getDataGenerator()->create_user(); $users[$user->id] = $user; $user = $this->getDataGenerator()->create_user(); $users[$user->id] = $user; $otheruser = $this->getDataGenerator()->create_user(); $ids = array_keys($users); $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); $uut->set_userids($ids); foreach ($uut as $key => $user) { $this->assertTrue(isset($users[$user->id])); $this->assertEquals($users[$user->id], $user); } } /** * Test that a deleted user is still returned. * If a user has data then it still must be deleted, even if they are deleted. * * @covers ::count */ public function test_current_user_one_user(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); $uut->set_userids([$user->id]); $this->assertCount(1, $uut); $this->assertEquals($user, $uut->current()); delete_user($user); $u = $uut->current(); $this->assertEquals($user->id, $u->id); } /** * Test that an invalid user returns no entry. * * @covers ::count */ public function test_current_user_invalid(): void { $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); $uut->set_userids([-100]); $this->assertCount(1, $uut); $this->assertNull($uut->current()); } /** * Test that where an invalid user is listed, the next user in the list is returned instead. * * @covers ::count */ public function test_current_user_two_users(): void { $this->resetAfterTest(); $u1 = $this->getDataGenerator()->create_user(); $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); $uut->set_userids([-100, $u1->id]); $this->assertCount(2, $uut); $this->assertEquals($u1, $uut->current()); } /** * Ensure that the component specified in the constructor is used and available. * * @covers ::set_component */ public function test_set_component_in_constructor(): void { $uut = new test_userlist_base(\context_system::instance(), 'core_tests'); $this->assertEquals('core_tests', $uut->get_component()); } /** * Ensure that the context specified in the constructor is available. * * @covers ::__construct */ public function test_set_context_in_constructor(): void { $context = \context_user::instance(\core_user::get_user_by_username('admin')->id); $uut = new test_userlist_base($context, 'core_tests'); $this->assertEquals($context, $uut->get_context()); } } /** * A test class extending the userlist_base allowing setting of the userids. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class test_userlist_base extends userlist_base { /** * Set the contextids for the test class. * * @param int[] $contexids The list of contextids to use. */ public function set_userids(array $userids): userlist_base { return parent::set_userids($userids); } } legacy_polyfill_test.php000064400000023407152265473400011510 0ustar00. /** * Unit tests for the Privacy API's legacy_polyfill. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\contextlist; use core_privacy\local\request\approved_contextlist; defined('MOODLE_INTERNAL') || die(); global $CFG; /** * Unit tests for the Privacy API's legacy_polyfill. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\legacy_polyfill */ final class legacy_polyfill_test extends \advanced_testcase { /** * Test that the null_provider polyfill works and that the static _get_reason can be * successfully called. * * @covers ::get_reason */ public function test_null_provider(): void { $this->assertEquals('thisisareason', test_legacy_polyfill_null_provider::get_reason()); } /** * Test that the metdata\provider polyfill works and that the static _get_metadata can be * successfully called. * * @covers ::get_metadata */ public function test_metadata_provider(): void { $collection = new collection('core_privacy'); $this->assertSame($collection, test_legacy_polyfill_metadata_provider::get_metadata($collection)); } /** * Test that the local\request\user_preference_provider polyfill works and that the static * _export_user_preferences can be successfully called. * * @covers ::export_user_preferences */ public function test_user_preference_provider(): void { $userid = 417; $mock = $this->createMock(test_legacy_polyfill_mock_wrapper::class); $mock->expects($this->once()) ->method('get_return_value') ->with('_export_user_preferences', [$userid]); test_legacy_polyfill_user_preference_provider::$mock = $mock; test_legacy_polyfill_user_preference_provider::export_user_preferences($userid); } /** * Test that the local\request\core_user_preference_provider polyfill works and that the static * _get_contexts_for_userid can be successfully called. * * @covers ::get_contexts_for_userid */ public function test_get_contexts_for_userid(): void { $userid = 417; $contextlist = new contextlist('core_privacy'); $mock = $this->createMock(test_legacy_polyfill_mock_wrapper::class); $mock->expects($this->once()) ->method('get_return_value') ->with('_get_contexts_for_userid', [$userid]) ->willReturn($contextlist); test_legacy_polyfill_request_provider::$mock = $mock; $result = test_legacy_polyfill_request_provider::get_contexts_for_userid($userid); $this->assertSame($contextlist, $result); } /** * Test that the local\request\core_user_preference_provider polyfill works and that the static * _export_user_data can be successfully called. * * @covers ::export_user_data */ public function test_export_user_data(): void { $contextlist = new approved_contextlist(\core_user::get_user_by_username('admin'), 'core_privacy', [98]); $mock = $this->createMock(test_legacy_polyfill_mock_wrapper::class); $mock->expects($this->once()) ->method('get_return_value') ->with('_export_user_data', [$contextlist]); test_legacy_polyfill_request_provider::$mock = $mock; test_legacy_polyfill_request_provider::export_user_data($contextlist); } /** * Test that the local\request\core_user_preference_provider polyfill works and that the static * _delete_data_for_all_users_in_context can be successfully called. * * @covers ::delete_data_for_all_users_in_context */ public function test_delete_data_for_all_users_in_context(): void { $mock = $this->createMock(test_legacy_polyfill_mock_wrapper::class); $mock->expects($this->once()) ->method('get_return_value') ->with('_delete_data_for_all_users_in_context', [\context_system::instance()]); test_legacy_polyfill_request_provider::$mock = $mock; test_legacy_polyfill_request_provider::delete_data_for_all_users_in_context(\context_system::instance()); } /** * Test that the local\request\core_user_preference_provider polyfill works and that the static * _delete_data_for_user can be successfully called. * * @covers ::delete_data_for_user */ public function test_delete_data_for_user(): void { $contextlist = new approved_contextlist(\core_user::get_user_by_username('admin'), 'core_privacy', [98]); $mock = $this->createMock(test_legacy_polyfill_mock_wrapper::class); $mock->expects($this->once()) ->method('get_return_value') ->with('_delete_data_for_user', [$contextlist]); test_legacy_polyfill_request_provider::$mock = $mock; test_legacy_polyfill_request_provider::delete_data_for_user($contextlist); } } /** * Legacy polyfill test for the null provider. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class test_legacy_polyfill_null_provider implements \core_privacy\local\metadata\null_provider { use \core_privacy\local\legacy_polyfill; /** * Test for get_reason */ protected static function _get_reason() { return 'thisisareason'; } } /** * Legacy polyfill test for the metadata provider. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class test_legacy_polyfill_metadata_provider implements \core_privacy\local\metadata\provider { use \core_privacy\local\legacy_polyfill; /** * Test for get_metadata. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ protected static function _get_metadata(collection $collection) { return $collection; } } /** * Legacy polyfill test for the metadata provider. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class test_legacy_polyfill_user_preference_provider implements \core_privacy\local\request\user_preference_provider { use \core_privacy\local\legacy_polyfill; /** * @var test_legacy_polyfill_request_provider $mock */ public static $mock = null; /** * Export all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ protected static function _export_user_preferences($userid) { return static::$mock->get_return_value(__FUNCTION__, func_get_args()); } } /** * Legacy polyfill test for the request provider. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class test_legacy_polyfill_request_provider implements \core_privacy\local\request\core_user_data_provider { use \core_privacy\local\legacy_polyfill; /** * @var test_legacy_polyfill_request_provider $mock */ public static $mock = null; /** * Test for get_contexts_for_userid. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ protected static function _get_contexts_for_userid($userid) { return static::$mock->get_return_value(__FUNCTION__, func_get_args()); } /** * Test for export_user_data. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ protected static function _export_user_data(approved_contextlist $contextlist) { return static::$mock->get_return_value(__FUNCTION__, func_get_args()); } /** * Delete all use data which matches the specified deletion criteria. * * @param context $context The specific context to delete data for. */ public static function _delete_data_for_all_users_in_context(\context $context) { return static::$mock->get_return_value(__FUNCTION__, func_get_args()); } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function _delete_data_for_user(approved_contextlist $contextlist) { return static::$mock->get_return_value(__FUNCTION__, func_get_args()); } } class test_legacy_polyfill_mock_wrapper { /** * Get the return value for the specified item. */ public function get_return_value() {} } userlist_collection_test.php000064400000012653152265473400012420 0ustar00. namespace core_privacy; use advanced_testcase; use \core_privacy\local\request\userlist_collection; use \core_privacy\local\request\userlist; use \core_privacy\local\request\approved_userlist; /** * Tests for the \core_privacy API's userlist collection functionality. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\userlist_collection */ final class userlist_collection_test extends advanced_testcase { /** * A userlist_collection should support the userlist type. * * @covers ::add_userlist */ public function test_supports_userlist(): void { $cut = \context_system::instance(); $uut = new userlist_collection($cut); $userlist = new userlist($cut, 'core_privacy'); $uut->add_userlist($userlist); $this->assertCount(1, $uut->get_userlists()); } /** * A userlist_collection should support the approved_userlist type. * * @covers ::add_userlist */ public function test_supports_approved_userlist(): void { $cut = \context_system::instance(); $uut = new userlist_collection($cut); $userlist = new approved_userlist($cut, 'core_privacy', [1, 2, 3]); $uut->add_userlist($userlist); $this->assertCount(1, $uut->get_userlists()); } /** * Ensure that get_userlist_for_component returns the correct userlist. * * @covers ::get_userlist_for_component */ public function test_get_userlist_for_component(): void { $cut = \context_system::instance(); $uut = new userlist_collection($cut); $privacy = new userlist($cut, 'core_privacy'); $uut->add_userlist($privacy); $test = new userlist($cut, 'core_tests'); $uut->add_userlist($test); // Note: This uses assertSame rather than assertEquals. // The former checks the actual object, whilst assertEquals only checks that they look the same. $this->assertSame($privacy, $uut->get_userlist_for_component('core_privacy')); $this->assertSame($test, $uut->get_userlist_for_component('core_tests')); } /** * Ensure that get_userlist_for_component does not die horribly when querying a non-existent component. * * @covers ::get_userlist_for_component */ public function test_get_userlist_for_component_not_found(): void { $cut = \context_system::instance(); $uut = new userlist_collection($cut); $this->assertNull($uut->get_userlist_for_component('core_tests')); } /** * Ensure that a duplicate userlist in the collection throws an Exception. * * @covers ::add_userlist */ public function test_duplicate_addition_throws(): void { $cut = \context_system::instance(); $uut = new userlist_collection($cut); $userlist = new userlist($cut, 'core_privacy'); $uut->add_userlist($userlist); $this->expectException('moodle_exception'); $uut->add_userlist($userlist); } /** * Ensure that the userlist_collection is countable. * * @covers ::count */ public function test_countable(): void { $cut = \context_system::instance(); $uut = new userlist_collection($cut); $uut->add_userlist(new userlist($cut, 'core_privacy')); $uut->add_userlist(new userlist($cut, 'core_tests')); $this->assertCount(2, $uut); } /** * Ensure that the userlist_collection iterates over the set of userlists. * * @covers ::current * @covers ::key * @covers ::next * @covers ::rewind * @covers ::valid */ public function test_iteration(): void { $cut = \context_system::instance(); $uut = new userlist_collection($cut); $testdata = []; $privacy = new userlist($cut, 'core_privacy'); $uut->add_userlist($privacy); $testdata['core_privacy'] = $privacy; $test = new userlist($cut, 'core_tests'); $uut->add_userlist($test); $testdata['core_tests'] = $test; $another = new userlist($cut, 'privacy_another'); $uut->add_userlist($another); $testdata['privacy_another'] = $another; foreach ($uut as $component => $list) { $this->assertEquals($testdata[$component], $list); } $this->assertCount(3, $uut); } /** * Test that the context is correctly returned. * * @covers ::get_context */ public function test_get_context(): void { $cut = \context_system::instance(); $uut = new userlist_collection($cut); $this->assertSame($cut, $uut->get_context()); } } types_plugintype_link_test.php000064400000006674152265473400013002 0ustar00. namespace core_privacy; use core_privacy\local\metadata\types\plugintype_link; /** * Tests for the \core_privacy API's types\plugintype_link functionality. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\metadata\types\plugintype_link */ final class types_plugintype_link_test extends \advanced_testcase { /** * Ensure that warnings are thrown if string identifiers contain invalid characters. * * @dataProvider invalid_string_provider * @param string $name Name * @param string $summary Summary * @covers ::__construct */ public function test_invalid_configs($name, $privacyfields, $summary): void { $record = new plugintype_link($name, $privacyfields, $summary); $this->assertDebuggingCalled(); } /** * Ensure that warnings are not thrown if debugging is not enabled, even if string identifiers contain invalid characters. * * @dataProvider invalid_string_provider * @param string $name Name * @param string $summary Summary * @covers ::__construct */ public function test_invalid_configs_debug_normal($name, $privacyfields, $summary): void { global $CFG; $this->resetAfterTest(); $CFG->debug = DEBUG_NORMAL; $record = new plugintype_link($name, $privacyfields, $summary); $this->assertDebuggingNotCalled(); } /** * Ensure that no warnings are shown for valid combinations. * * @dataProvider valid_string_provider * @param string $name Name * @param string $summary Summary * @covers ::__construct */ public function test_valid_configs($name, $privacyfields, $summary): void { $record = new plugintype_link($name, $privacyfields, $summary); $this->assertDebuggingNotCalled(); } /** * Data provider with a list of invalid string identifiers. * * @return array */ public static function invalid_string_provider(): array { return [ 'Space in summary' => [ 'example', [], 'This table is used for purposes.', ], 'Comma in summary' => [ 'example', [], 'privacy,foo', ], ]; } /** * Data provider with a list of valid string identifiers. * * @return array */ public static function valid_string_provider(): array { return [ 'Valid combination' => [ 'example', [], 'privacy:example:valid', ], ]; } } collection_test.php000064400000015665152265473400010474 0ustar00. namespace core_privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; use core_privacy\local\metadata\collection; use core_privacy\local\metadata\types; /** * Tests for the \core_privacy API's collection functionality. * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\metadata\collection */ final class collection_test extends \advanced_testcase { /** * Test that adding an unknown type causes the type to be added to the collection. * * @covers ::add_type */ public function test_add_type_generic_type(): void { $collection = new collection('core_privacy'); // Mock a new types\type. $mockedtype = $this->createMock(types\type::class); $collection->add_type($mockedtype); $items = $collection->get_collection(); $this->assertCount(1, $items); $this->assertEquals($mockedtype, reset($items)); } /** * Test that adding a known type works as anticipated. * * @covers ::add_type */ public function test_add_type_known_type(): void { $collection = new collection('core_privacy'); $linked = new types\subsystem_link('example', [], 'langstring'); $collection->add_type($linked); $items = $collection->get_collection(); $this->assertCount(1, $items); $this->assertEquals($linked, reset($items)); } /** * Test that adding multiple types returns them all. * * @covers ::add_type */ public function test_add_type_multiple(): void { $collection = new collection('core_privacy'); $a = new types\subsystem_link('example', [], 'langstring'); $collection->add_type($a); $b = new types\subsystem_link('example', [], 'langstring'); $collection->add_type($b); $items = $collection->get_collection(); $this->assertCount(2, $items); } /** * Test that the add_database_table function adds a database table. * * @covers ::add_database_table */ public function test_add_database_table(): void { $collection = new collection('core_privacy'); $name = 'example'; $fields = ['field' => 'description']; $summary = 'summarisation'; $collection->add_database_table($name, $fields, $summary); $items = $collection->get_collection(); $this->assertCount(1, $items); $item = reset($items); $this->assertInstanceOf(types\database_table::class, $item); $this->assertEquals($name, $item->get_name()); $this->assertEquals($fields, $item->get_privacy_fields()); $this->assertEquals($summary, $item->get_summary()); } /** * Test that the add_user_preference function adds a single user preference. * * @covers ::add_user_preference */ public function test_add_user_preference(): void { $collection = new collection('core_privacy'); $name = 'example'; $summary = 'summarisation'; $collection->add_user_preference($name, $summary); $items = $collection->get_collection(); $this->assertCount(1, $items); $item = reset($items); $this->assertInstanceOf(types\user_preference::class, $item); $this->assertEquals($name, $item->get_name()); $this->assertEquals($summary, $item->get_summary()); } /** * Test that the link_external_location function links an external location. * * @covers ::link_external_location */ public function test_link_external_location(): void { $collection = new collection('core_privacy'); $name = 'example'; $fields = ['field' => 'description']; $summary = 'summarisation'; $collection->link_external_location($name, $fields, $summary); $items = $collection->get_collection(); $this->assertCount(1, $items); $item = reset($items); $this->assertInstanceOf(types\external_location::class, $item); $this->assertEquals($name, $item->get_name()); $this->assertEquals($fields, $item->get_privacy_fields()); $this->assertEquals($summary, $item->get_summary()); } /** * Test that the link_subsystem function links the subsystem. * * @covers ::link_subsystem */ public function test_link_subsystem(): void { $collection = new collection('core_privacy'); $name = 'example'; $summary = 'summarisation'; $collection->link_subsystem($name, $summary); $items = $collection->get_collection(); $this->assertCount(1, $items); $item = reset($items); $this->assertInstanceOf(types\subsystem_link::class, $item); $this->assertEquals($name, $item->get_name()); $this->assertEquals($summary, $item->get_summary()); } /** * Test that the link_plugintype function links the plugin. * * @covers ::link_plugintype */ public function test_link_plugintype(): void { $collection = new collection('core_privacy'); $name = 'example'; $summary = 'summarisation'; $collection->link_plugintype($name, $summary); $items = $collection->get_collection(); $this->assertCount(1, $items); $item = reset($items); $this->assertInstanceOf(types\plugintype_link::class, $item); $this->assertEquals($name, $item->get_name()); $this->assertEquals($summary, $item->get_summary()); } /** * Data provider to supply a list of valid components. * * @return array */ public static function component_list_provider(): array { return [ ['core_privacy'], ['mod_forum'], ]; } /** * Test that we can get the component correctly. * * The component will be used for string translations. * * @dataProvider component_list_provider * @param string $component The component to test * @covers ::get_component */ public function test_get_component($component): void { $collection = new collection($component); $this->assertEquals($component, $collection->get_component()); } } approved_userlist_test.php000064400000006675152265473400012114 0ustar00. /** * Unit Tests for the approved userlist Class * * @package core_privacy * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; use \core_privacy\local\request\approved_userlist; use \core_privacy\local\request\userlist; /** * Tests for the \core_privacy API's approved userlist functionality. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_privacy\local\request\approved_userlist */ final class approved_userlist_test extends advanced_testcase { /** * The approved userlist should not be modifiable once set. * * @covers ::__construct * @covers \core_privacy\local\request\approved_userlist */ public function test_default_values_set(): void { $this->resetAfterTest(); $u1 = $this->getDataGenerator()->create_user(); $u2 = $this->getDataGenerator()->create_user(); $u3 = $this->getDataGenerator()->create_user(); $u4 = $this->getDataGenerator()->create_user(); $context = \context_system::instance(); $component = 'core_privacy'; $uut = new approved_userlist($context, $component, [$u1->id, $u2->id]); $this->assertEquals($context, $uut->get_context()); $this->assertEquals($component, $uut->get_component()); $expected = [ $u1->id, $u2->id, ]; sort($expected); $result = $uut->get_userids(); sort($result); $this->assertEquals($expected, $result); } /** * @covers ::create_from_userlist * @covers \core_privacy\local\request\approved_userlist */ public function test_create_from_userlist(): void { $this->resetAfterTest(); $u1 = $this->getDataGenerator()->create_user(); $u2 = $this->getDataGenerator()->create_user(); $u3 = $this->getDataGenerator()->create_user(); $u4 = $this->getDataGenerator()->create_user(); $context = \context_system::instance(); $component = 'core_privacy'; $sourcelist = new userlist($context, $component); $sourcelist->add_users([$u1->id, $u3->id]); $expected = [ $u1->id, $u3->id, ]; sort($expected); $approvedlist = approved_userlist::create_from_userlist($sourcelist); $this->assertEquals($component, $approvedlist->get_component()); $this->assertEquals($context, $approvedlist->get_context()); $result = $approvedlist->get_userids(); sort($result); $this->assertEquals($expected, $result); } } manager_test.php000064400000011705152265473400007742 0ustar00. namespace mod_subsection; use availability_date\condition; use core_availability\tree; use core_courseformat\formatactions; /** * Tests for Subsection manager class. * * @covers \mod_subsection\manager * @package mod_subsection * @category test * @copyright 2024 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class manager_test extends \advanced_testcase { /** * Test get_delegated_section_info. * * @covers ::get_delegated_section_info * @dataProvider provider_test_get_delegated_section_info * @param bool $hasavailability Whether the module has access restrictions. * @param bool $visible Whether the module is visible. * @return void */ public function test_get_delegated_section_info( bool $hasavailability, bool $visible ): void { global $DB; $this->resetAfterTest(); // Set up the availability settings. $availabilityjson = null; if ($hasavailability) { $operation = condition::DIRECTION_FROM; $availabilityjson = json_encode(tree::get_root_json( [ condition::get_json($operation, time() + 3600), ], '&', true )); } $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 2]); $module = $this->getDataGenerator()->create_module( 'subsection', (object)['course' => $course->id, 'section' => 2], ['visible' => $visible, 'availability' => $availabilityjson] ); $cm = get_coursemodule_from_id('subsection', $module->cmid, 0, false, MUST_EXIST); $manager = manager::create_from_coursemodule($cm); $sectioninfo = $manager->get_delegated_section_info(); $this->assertInstanceOf(\section_info::class, $sectioninfo); $this->assertEquals($cm->instance, $sectioninfo->itemid); $this->assertEquals($cm->name, $sectioninfo->name); $this->assertEquals($cm->visible, $sectioninfo->visible); $this->assertEquals($cm->availability, $sectioninfo->availability); $initialid = $sectioninfo->id; // When subsections are disabled, all subsections are considered orphaned // and can be removed without affecting the course_module. This should regenerate // the delegated section once the module is re-enabled. $pluginmanager = \core_plugin_manager::resolve_plugininfo_class('mod'); $pluginmanager::enable_plugin('subsection', 0); formatactions::section($course)->delete($sectioninfo); rebuild_course_cache($course->id, true); $pluginmanager::enable_plugin('subsection', 1); rebuild_course_cache($course->id, true); $cm = get_coursemodule_from_id('subsection', $module->cmid, 0, false, MUST_EXIST); $manager = manager::create_from_coursemodule($cm); $sectioninfo = $manager->get_delegated_section_info(); $this->assertInstanceOf(\section_info::class, $sectioninfo); $this->assertEquals($cm->instance, $sectioninfo->itemid); $this->assertEquals($cm->name, $sectioninfo->name); $this->assertEquals($cm->visible, $sectioninfo->visible); $this->assertEquals($cm->availability, $sectioninfo->availability); // The section should be different from the previous one. $this->assertNotEquals($initialid, $sectioninfo->id); } /** * Data provider for test_get_delegated_section_info. * * @return array */ public static function provider_test_get_delegated_section_info(): array { return [ 'Module is visible with no restrictions' => [ 'hasavailability' => false, 'visible' => true, ], 'Module is visible with restrictions' => [ 'hasavailability' => true, 'visible' => true, ], 'Module is hidden with no restrictions' => [ 'hasavailability' => false, 'visible' => false, ], 'Module is hidden with restrictions' => [ 'hasavailability' => true, 'visible' => false, ], ]; } } db_test.php000064400000056231152265517440006723 0ustar00. namespace auth_db; /** * External database auth sync tests, this also tests adodb drivers * that are matching our four supported Moodle database drivers. * * @package auth_db * @category phpunit * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class db_test extends \advanced_testcase { /** @var string Original error log */ protected $oldlog; /** @var int The amount of users to create for the large user set deletion test */ protected $largedeletionsetsize = 128; public static function tearDownAfterClass(): void { global $DB; // Apply sqlsrv native driver error and logging default // settings while finishing the AdoDB tests. if ($DB->get_dbfamily() === 'mssql') { sqlsrv_configure("WarningsReturnAsErrors", false); sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_OFF); sqlsrv_configure("LogSeverity", SQLSRV_LOG_SEVERITY_ERROR); } parent::tearDownAfterClass(); } protected function init_auth_database() { global $DB, $CFG; require_once("$CFG->dirroot/auth/db/auth.php"); // Discard error logs from AdoDB. $this->oldlog = ini_get('error_log'); ini_set('error_log', "$CFG->dataroot/testlog.log"); $dbman = $DB->get_manager(); set_config('extencoding', 'utf-8', 'auth_db'); set_config('host', $CFG->dbhost, 'auth_db'); set_config('user', $CFG->dbuser, 'auth_db'); set_config('pass', $CFG->dbpass, 'auth_db'); set_config('name', $CFG->dbname, 'auth_db'); if (!empty($CFG->dboptions['dbport'])) { set_config('host', $CFG->dbhost.':'.$CFG->dboptions['dbport'], 'auth_db'); } switch ($DB->get_dbfamily()) { case 'mysql': set_config('type', 'mysqli', 'auth_db'); set_config('setupsql', "SET NAMES 'UTF-8'", 'auth_db'); set_config('sybasequoting', '0', 'auth_db'); if (!empty($CFG->dboptions['dbsocket'])) { $dbsocket = $CFG->dboptions['dbsocket']; if ((strpos($dbsocket, '/') === false and strpos($dbsocket, '\\') === false)) { $dbsocket = ini_get('mysqli.default_socket'); } set_config('type', 'mysqli://'.rawurlencode($CFG->dbuser).':'.rawurlencode($CFG->dbpass).'@'.rawurlencode($CFG->dbhost).'/'.rawurlencode($CFG->dbname).'?socket='.rawurlencode($dbsocket), 'auth_db'); } break; case 'postgres': set_config('type', 'postgres7', 'auth_db'); $setupsql = "SET NAMES 'UTF-8'"; if (!empty($CFG->dboptions['dbschema'])) { $setupsql .= "; SET search_path = '".$CFG->dboptions['dbschema']."'"; } set_config('setupsql', $setupsql, 'auth_db'); set_config('sybasequoting', '0', 'auth_db'); if (!empty($CFG->dboptions['dbsocket']) and ($CFG->dbhost === 'localhost' or $CFG->dbhost === '127.0.0.1')) { if (strpos($CFG->dboptions['dbsocket'], '/') !== false) { $socket = $CFG->dboptions['dbsocket']; if (!empty($CFG->dboptions['dbport'])) { $socket .= ':' . $CFG->dboptions['dbport']; } set_config('host', $socket, 'auth_db'); } else { set_config('host', '', 'auth_db'); } } break; case 'mssql': set_config('type', 'mssqlnative', 'auth_db'); set_config('sybasequoting', '1', 'auth_db'); // The native sqlsrv driver uses a comma as separator between host and port. $dbhost = $CFG->dbhost; if (!empty($dboptions['dbport'])) { $dbhost .= ',' . $dboptions['dbport']; } set_config('host', $dbhost, 'auth_db'); break; default: throw new exception('Unknown database family ' . $DB->get_dbfamily()); } $table = new \xmldb_table('auth_db_users'); $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('pass', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('email', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('firstname', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('lastname', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('animal', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); if ($dbman->table_exists($table)) { $dbman->drop_table($table); } $dbman->create_table($table); set_config('table', $CFG->prefix.'auth_db_users', 'auth_db'); set_config('fielduser', 'name', 'auth_db'); set_config('fieldpass', 'pass', 'auth_db'); set_config('field_map_lastname', 'lastname', 'auth_db'); set_config('field_updatelocal_lastname', 'oncreate', 'auth_db'); set_config('field_lock_lastname', 'unlocked', 'auth_db'); // Setu up field mappings. set_config('field_map_email', 'email', 'auth_db'); set_config('field_updatelocal_email', 'oncreate', 'auth_db'); set_config('field_updateremote_email', '0', 'auth_db'); set_config('field_lock_email', 'unlocked', 'auth_db'); // Create a user profile field and add mapping to it. $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'pet', 'name' => 'Pet', 'datatype' => 'text']); set_config('field_map_profile_field_pet', 'animal', 'auth_db'); set_config('field_updatelocal_profile_field_pet', 'oncreate', 'auth_db'); set_config('field_updateremote_profile_field_pet', '0', 'auth_db'); set_config('field_lock_profile_field_pet', 'unlocked', 'auth_db'); // Init the rest of settings. set_config('passtype', 'plaintext', 'auth_db'); set_config('changepasswordurl', '', 'auth_db'); set_config('debugauthdb', 0, 'auth_db'); set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_db'); } protected function cleanup_auth_database() { global $DB; $dbman = $DB->get_manager(); $table = new \xmldb_table('auth_db_users'); $dbman->drop_table($table); ini_set('error_log', $this->oldlog); } public function test_plugin(): void { global $DB, $CFG; require_once($CFG->dirroot . '/user/profile/lib.php'); $this->resetAfterTest(true); // NOTE: It is strongly discouraged to create new tables in advanced_testcase classes, // but there is no other simple way to test ext database enrol sync, so let's // disable transactions are try to cleanup after the tests. $this->preventResetByRollback(); $this->init_auth_database(); /** @var auth_plugin_db $auth */ $auth = get_auth_plugin('db'); $authdb = $auth->db_init(); // Test adodb may access the table. $user1 = (object)array('name'=>'u1', 'pass'=>'heslo', 'email'=>'u1@example.com'); $user1->id = $DB->insert_record('auth_db_users', $user1); $sql = "SELECT * FROM {$auth->config->table}"; $rs = $authdb->Execute($sql); $this->assertInstanceOf('ADORecordSet', $rs); $this->assertFalse($rs->EOF); $fields = $rs->FetchRow(); $this->assertTrue(is_array($fields)); $this->assertTrue($rs->EOF); $rs->Close(); $authdb->Close(); // Test bulk user account creation. $user2 = (object)['name' => 'u2', 'pass' => 'heslo', 'email' => 'u2@example.com', 'animal' => 'cat']; $user2->id = $DB->insert_record('auth_db_users', $user2); $user3 = (object)array('name'=>'admin', 'pass'=>'heslo', 'email'=>'admin@example.com'); // Should be skipped. $user3->id = $DB->insert_record('auth_db_users', $user3); $this->assertCount(2, $DB->get_records('user')); $trace = new \null_progress_trace(); // Sync users and make sure that two events user_created werer triggered. $sink = $this->redirectEvents(); $auth->sync_users($trace, false); $events = $sink->get_events(); $sink->close(); $this->assertCount(2, $events); $this->assertTrue($events[0] instanceof \core\event\user_created); $this->assertTrue($events[1] instanceof \core\event\user_created); // Assert the two users were created. $this->assertEquals(4, $DB->count_records('user')); $u1 = $DB->get_record('user', array('username'=>$user1->name, 'auth'=>'db')); $this->assertSame($user1->email, $u1->email); $this->assertEmpty(profile_user_record($u1->id)->pet); $u2 = $DB->get_record('user', array('username'=>$user2->name, 'auth'=>'db')); $this->assertSame($user2->email, $u2->email); $this->assertSame($user2->animal, profile_user_record($u2->id)->pet); $admin = $DB->get_record('user', array('username'=>'admin', 'auth'=>'manual')); $this->assertNotEmpty($admin); // Test sync updates. $user2b = clone($user2); $user2b->email = 'u2b@example.com'; $user2b->animal = 'dog'; $DB->update_record('auth_db_users', $user2b); $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $u2 = $DB->get_record('user', array('username'=>$user2->name)); $this->assertSame($user2->email, $u2->email); $this->assertSame($user2->animal, profile_user_record($u2->id)->pet); $auth->sync_users($trace, true); $this->assertEquals(4, $DB->count_records('user')); $u2 = $DB->get_record('user', array('username'=>$user2->name)); $this->assertSame($user2->email, $u2->email); set_config('field_updatelocal_email', 'onlogin', 'auth_db'); $auth->config->field_updatelocal_email = 'onlogin'; set_config('field_updatelocal_profile_field_pet', 'onlogin', 'auth_db'); $auth->config->field_updatelocal_profile_field_pet = 'onlogin'; $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $u2 = $DB->get_record('user', array('username'=>$user2->name)); $this->assertSame($user2->email, $u2->email); $auth->sync_users($trace, true); $this->assertEquals(4, $DB->count_records('user')); $u2 = $DB->get_record('user', array('username'=>$user2->name)); $this->assertSame($user2b->email, $u2->email); $this->assertSame($user2b->animal, profile_user_record($u2->id)->pet); // Test sync deletes and suspends. $DB->delete_records('auth_db_users', array('id'=>$user2->id)); $this->assertCount(2, $DB->get_records('auth_db_users')); unset($user2); unset($user2b); $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); set_config('removeuser', AUTH_REMOVEUSER_SUSPEND, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_SUSPEND; $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(1, $DB->count_records('user', array('suspended'=>1))); $user2 = (object)array('name'=>'u2', 'pass'=>'heslo', 'email'=>'u2@example.com'); $user2->id = $DB->insert_record('auth_db_users', $user2); $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $DB->delete_records('auth_db_users', array('id'=>$user2->id)); set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_FULLDELETE; $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $user2 = (object)array('name'=>'u2', 'pass'=>'heslo', 'email'=>'u2@example.com'); $user2->id = $DB->insert_record('auth_db_users', $user2); $auth->sync_users($trace, false); $this->assertEquals(5, $DB->count_records('user')); $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); // Test user_login(). $user3 = (object)array('name'=>'u3', 'pass'=>'heslo', 'email'=>'u3@example.com'); $user3->id = $DB->insert_record('auth_db_users', $user3); $this->assertFalse($auth->user_login('u4', 'heslo')); $this->assertTrue($auth->user_login('u1', 'heslo')); $this->assertFalse($DB->record_exists('user', array('username'=>'u3', 'auth'=>'db'))); $this->assertTrue($auth->user_login('u3', 'heslo')); $this->assertFalse($DB->record_exists('user', array('username'=>'u3', 'auth'=>'db'))); set_config('passtype', 'md5', 'auth_db'); $auth->config->passtype = 'md5'; $user3->pass = md5('heslo'); $DB->update_record('auth_db_users', $user3); $this->assertTrue($auth->user_login('u3', 'heslo')); // Test user created to see if the checking happens strictly. $usermd5 = (object)['name' => 'usermd5', 'pass' => '0e462097431906509019562988736854']; $usermd5->id = $DB->insert_record('auth_db_users', $usermd5); // md5('240610708') === '0e462097431906509019562988736854'. $this->assertTrue($auth->user_login('usermd5', '240610708')); $this->assertFalse($auth->user_login('usermd5', 'QNKCDZO')); set_config('passtype', 'sh1', 'auth_db'); $auth->config->passtype = 'sha1'; $user3->pass = sha1('heslo'); $DB->update_record('auth_db_users', $user3); $this->assertTrue($auth->user_login('u3', 'heslo')); // Test user created to see if the checking happens strictly. $usersha1 = (object)['name' => 'usersha1', 'pass' => '0e66507019969427134894567494305185566735']; $usersha1->id = $DB->insert_record('auth_db_users', $usersha1); // sha1('aaroZmOk') === '0e66507019969427134894567494305185566735'. $this->assertTrue($auth->user_login('usersha1', 'aaroZmOk')); $this->assertFalse($auth->user_login('usersha1', 'aaK1STfY')); set_config('passtype', 'saltedcrypt', 'auth_db'); $auth->config->passtype = 'saltedcrypt'; $user3->pass = password_hash('heslo', PASSWORD_BCRYPT); $DB->update_record('auth_db_users', $user3); $this->assertTrue($auth->user_login('u3', 'heslo')); set_config('passtype', 'internal', 'auth_db'); $auth->config->passtype = 'internal'; create_user_record('u3', 'heslo', 'db'); $this->assertTrue($auth->user_login('u3', 'heslo')); $DB->delete_records('auth_db_users', array('id'=>$user3->id)); set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_KEEP; $this->assertTrue($auth->user_login('u3', 'heslo')); set_config('removeuser', AUTH_REMOVEUSER_SUSPEND, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_SUSPEND; $this->assertFalse($auth->user_login('u3', 'heslo')); set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_FULLDELETE; $this->assertFalse($auth->user_login('u3', 'heslo')); set_config('passtype', 'sh1', 'auth_db'); $auth->config->passtype = 'sha1'; $this->assertFalse($auth->user_login('u3', 'heslo')); // Test login create and update. $user4 = (object)array('name'=>'u4', 'pass'=>'heslo', 'email'=>'u4@example.com'); $user4->id = $DB->insert_record('auth_db_users', $user4); set_config('passtype', 'plaintext', 'auth_db'); $auth->config->passtype = 'plaintext'; $iuser4 = create_user_record('u4', 'heslo', 'db'); $this->assertNotEmpty($iuser4); $this->assertSame($user4->name, $iuser4->username); $this->assertSame($user4->email, $iuser4->email); $this->assertSame('db', $iuser4->auth); $this->assertSame($CFG->mnet_localhost_id, $iuser4->mnethostid); $user4b = clone($user4); $user4b->email = 'u4b@example.com'; $DB->update_record('auth_db_users', $user4b); set_config('field_updatelocal_email', 'oncreate', 'auth_db'); $auth->config->field_updatelocal_email = 'oncreate'; update_user_record('u4'); $iuser4 = $DB->get_record('user', array('id'=>$iuser4->id)); $this->assertSame($user4->email, $iuser4->email); set_config('field_updatelocal_email', 'onlogin', 'auth_db'); $auth->config->field_updatelocal_email = 'onlogin'; update_user_record('u4'); $iuser4 = $DB->get_record('user', array('id'=>$iuser4->id)); $this->assertSame($user4b->email, $iuser4->email); // Test user_exists() $this->assertTrue($auth->user_exists('u1')); $this->assertTrue($auth->user_exists('admin')); $this->assertFalse($auth->user_exists('u3')); $this->assertTrue($auth->user_exists('u4')); $this->cleanup_auth_database(); } /** * Testing the function _colonscope() from ADOdb. */ public function test_adodb_colonscope(): void { global $CFG; require_once($CFG->libdir.'/adodb/adodb.inc.php'); require_once($CFG->libdir.'/adodb/drivers/adodb-odbc.inc.php'); require_once($CFG->libdir.'/adodb/drivers/adodb-db2ora.inc.php'); $this->resetAfterTest(false); $sql = "select * from table WHERE column=:1 AND anothercolumn > :0"; $arr = array('b', 1); list($sqlout, $arrout) = _colonscope($sql,$arr); $this->assertEquals("select * from table WHERE column=? AND anothercolumn > ?", $sqlout); $this->assertEquals(array(1, 'b'), $arrout); } /** * Testing the clean_data() method. */ public function test_clean_data(): void { global $DB; $this->resetAfterTest(false); $this->preventResetByRollback(); $this->init_auth_database(); $auth = get_auth_plugin('db'); $auth->db_init(); // Create users on external table. $extdbuser1 = (object)array('name'=>'u1', 'pass'=>'heslo', 'email'=>'u1@example.com'); $extdbuser1->id = $DB->insert_record('auth_db_users', $extdbuser1); // User with malicious data on the name (won't be imported). $extdbuser2 = (object)array('name'=>'userxss', 'pass'=>'heslo', 'email'=>'xssuser@example.com'); $extdbuser2->id = $DB->insert_record('auth_db_users', $extdbuser2); $extdbuser3 = (object)array('name'=>'u3', 'pass'=>'heslo', 'email'=>'u3@example.com', 'lastname' => 'userxss'); $extdbuser3->id = $DB->insert_record('auth_db_users', $extdbuser3); $trace = new \null_progress_trace(); // Let's test user sync make sure still works as expected.. $auth->sync_users($trace, true); $this->assertDebuggingCalled("The property 'lastname' has invalid data and has been cleaned."); // User with correct data, should be equal to external db. $user1 = $DB->get_record('user', array('email'=> $extdbuser1->email, 'auth'=>'db')); $this->assertEquals($extdbuser1->name, $user1->username); $this->assertEquals($extdbuser1->email, $user1->email); // Get the user on moodle user table. $user2 = $DB->get_record('user', array('email'=> $extdbuser2->email, 'auth'=>'db')); $user3 = $DB->get_record('user', array('email'=> $extdbuser3->email, 'auth'=>'db')); $this->assertEmpty($user2); $this->assertEquals($extdbuser3->name, $user3->username); $this->assertEquals('useralert(1);xss', $user3->lastname); $this->cleanup_auth_database(); } /** * Testing the deletion of a user when there are many users in the external DB. */ public function test_deleting_with_many_users(): void { global $DB; $this->resetAfterTest(true); $this->preventResetByRollback(); $this->init_auth_database(); $auth = get_auth_plugin('db'); $auth->db_init(); // Set to delete from moodle when missing from DB. set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_FULLDELETE; // Create users. $users = []; for ($i = 0; $i < $this->largedeletionsetsize; $i++) { $user = (object)array('username' => "u$i", 'name' => "u$i", 'pass' => 'heslo', 'email' => "u$i@example.com"); $user->id = $DB->insert_record('auth_db_users', $user); $users[] = $user; } // Sync to moodle. $trace = new \null_progress_trace(); $auth->sync_users($trace, true); // Check user is there. $user = array_shift($users); $moodleuser = $DB->get_record('user', array('email' => $user->email, 'auth' => 'db')); $this->assertNotNull($moodleuser); $this->assertEquals($user->username, $moodleuser->username); // Delete a user. $DB->delete_records('auth_db_users', array('id' => $user->id)); // Sync again. $auth->sync_users($trace, true); // Check user is no longer there. $moodleuser = $DB->get_record('user', array('id' => $moodleuser->id)); $this->assertFalse($auth->user_login($user->username, 'heslo')); $this->assertEquals(1, $moodleuser->deleted); // Make sure it was the only user deleted. $numberdeleted = $DB->count_records('user', array('deleted' => 1, 'auth' => 'db')); $this->assertEquals(1, $numberdeleted); $this->cleanup_auth_database(); } } external/external_test.php000064400000242513152265517610012001 0ustar00. /** * External glossary functions unit tests * * @package mod_glossary * @category external * @copyright 2015 Costantino Cito * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\external; use core_external\external_api; use core_external\util as external_util; use externallib_advanced_testcase; use mod_glossary_external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * External glossary functions unit tests * * @package mod_glossary * @category external * @copyright 2015 Costantino Cito * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class external_test extends externallib_advanced_testcase { /** * Test get_glossaries_by_courses */ public function test_get_glossaries_by_courses(): void { $this->resetAfterTest(true); // As admin. $this->setAdminUser(); $c1 = self::getDataGenerator()->create_course(); $c2 = self::getDataGenerator()->create_course(); $g1 = self::getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'name' => 'First Glossary')); $g2 = self::getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'name' => 'Second Glossary')); $g3 = self::getDataGenerator()->create_module('glossary', array('course' => $c2->id, 'name' => 'Third Glossary')); $s1 = $this->getDataGenerator()->create_user(); self::getDataGenerator()->enrol_user($s1->id, $c1->id); // Check results where student is enrolled. $this->setUser($s1); $glossaries = mod_glossary_external::get_glossaries_by_courses(array()); $glossaries = external_api::clean_returnvalue(mod_glossary_external::get_glossaries_by_courses_returns(), $glossaries); $this->assertCount(2, $glossaries['glossaries']); $this->assertEquals('First Glossary', $glossaries['glossaries'][0]['name']); $this->assertEquals('Second Glossary', $glossaries['glossaries'][1]['name']); $this->assertEquals(1, $glossaries['glossaries'][0]['canaddentry']); $this->assertEquals(1, $glossaries['glossaries'][1]['canaddentry']); // Check results with specific course IDs. $glossaries = mod_glossary_external::get_glossaries_by_courses(array($c1->id, $c2->id)); $glossaries = external_api::clean_returnvalue(mod_glossary_external::get_glossaries_by_courses_returns(), $glossaries); $this->assertCount(2, $glossaries['glossaries']); $this->assertEquals('First Glossary', $glossaries['glossaries'][0]['name']); $this->assertEquals('Second Glossary', $glossaries['glossaries'][1]['name']); $this->assertEquals('course', $glossaries['warnings'][0]['item']); $this->assertEquals($c2->id, $glossaries['warnings'][0]['itemid']); $this->assertEquals('1', $glossaries['warnings'][0]['warningcode']); $this->assertEquals(1, $glossaries['glossaries'][0]['canaddentry']); // Now as admin. $this->setAdminUser(); $glossaries = mod_glossary_external::get_glossaries_by_courses(array($c2->id)); $glossaries = external_api::clean_returnvalue(mod_glossary_external::get_glossaries_by_courses_returns(), $glossaries); $this->assertCount(1, $glossaries['glossaries']); $this->assertEquals('Third Glossary', $glossaries['glossaries'][0]['name']); $this->assertEquals(1, $glossaries['glossaries'][0]['canaddentry']); } public function test_view_glossary(): void { $this->resetAfterTest(true); // Generate all the things. $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $sink = $this->redirectEvents(); $this->setUser($u1); $return = mod_glossary_external::view_glossary($g1->id, 'letter'); $return = external_api::clean_returnvalue(mod_glossary_external::view_glossary_returns(), $return); $events = $sink->get_events(); // Assertion. $this->assertTrue($return['status']); $this->assertEmpty($return['warnings']); $this->assertCount(1, $events); $this->assertEquals('\mod_glossary\event\course_module_viewed', $events[0]->eventname); $sink->close(); } public function test_view_glossary_without_permission(): void { $this->resetAfterTest(true); // Generate all the things. $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $ctx = \context_module::instance($g1->cmid); // Revoke permission. $roles = get_archetype_roles('user'); $role = array_shift($roles); assign_capability('mod/glossary:view', CAP_PROHIBIT, $role->id, $ctx, true); accesslib_clear_all_caches_for_unit_testing(); // Assertion. $this->setUser($u1); $this->expectException(\require_login_exception::class); $this->expectExceptionMessage('Activity is hidden'); mod_glossary_external::view_glossary($g1->id, 'letter'); } public function test_view_entry(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'visible' => false)); $u1 = $this->getDataGenerator()->create_user(); $e1 = $gg->create_content($g1, array('approved' => 1)); $e2 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u1->id)); $e3 = $gg->create_content($g1, array('approved' => 0, 'userid' => -1)); $e4 = $gg->create_content($g2, array('approved' => 1)); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $this->setUser($u1); // Test readable entry. $sink = $this->redirectEvents(); $return = mod_glossary_external::view_entry($e1->id); $return = external_api::clean_returnvalue(mod_glossary_external::view_entry_returns(), $return); $events = $sink->get_events(); $this->assertTrue($return['status']); $this->assertEmpty($return['warnings']); $this->assertCount(1, $events); $this->assertEquals('\mod_glossary\event\entry_viewed', $events[0]->eventname); $sink->close(); // Test non-approved of self. $return = mod_glossary_external::view_entry($e2->id); $return = external_api::clean_returnvalue(mod_glossary_external::view_entry_returns(), $return); $events = $sink->get_events(); $this->assertTrue($return['status']); $this->assertEmpty($return['warnings']); $this->assertCount(1, $events); $this->assertEquals('\mod_glossary\event\entry_viewed', $events[0]->eventname); $sink->close(); // Test non-approved of other. try { mod_glossary_external::view_entry($e3->id); $this->fail('Cannot view non-approved entries of others.'); } catch (\invalid_parameter_exception $e) { // All good. } // Test non-readable entry. $this->expectException(\require_login_exception::class); $this->expectExceptionMessage('Activity is hidden'); mod_glossary_external::view_entry($e4->id); } public function test_get_entries_by_letter(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $e1a = $gg->create_content($g1, array('approved' => 0, 'concept' => 'Bob', 'userid' => 2, 'tags' => array('Cats', 'Dogs'))); $e1b = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Jane', 'userid' => 2)); $e1c = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Alice', 'userid' => $u1->id)); $e1d = $gg->create_content($g1, array('approved' => 0, 'concept' => '0-day', 'userid' => $u1->id)); $e2a = $gg->create_content($g2); $this->setAdminUser(); // Just a normal request from admin user. $return = mod_glossary_external::get_entries_by_letter($g1->id, 'ALL', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1c->id, $return['entries'][0]['id']); $this->assertEquals($e1a->id, $return['entries'][1]['id']); $this->assertEquals('Cats', $return['entries'][1]['tags'][0]['rawname']); $this->assertEquals('Dogs', $return['entries'][1]['tags'][1]['rawname']); $this->assertEquals($e1b->id, $return['entries'][2]['id']); // An admin user requesting all the entries. $return = mod_glossary_external::get_entries_by_letter($g1->id, 'ALL', 0, 20, array('includenotapproved' => 1)); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1a->id, $return['entries'][2]['id']); $this->assertEquals($e1b->id, $return['entries'][3]['id']); // A normal user. $this->setUser($u1); $return = mod_glossary_external::get_entries_by_letter($g1->id, 'ALL', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1b->id, $return['entries'][2]['id']); // A normal user requesting to view all non approved entries. $return = mod_glossary_external::get_entries_by_letter($g1->id, 'ALL', 0, 20, array('includenotapproved' => 1)); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1b->id, $return['entries'][2]['id']); } public function test_get_entries_by_letter_with_parameters(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $e1a = $gg->create_content($g1, array('approved' => 1, 'concept' => '0-day', 'userid' => $u1->id)); $e1b = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Bob', 'userid' => 2)); $e1c = $gg->create_content($g1, array('approved' => 1, 'concept' => '1-dayb', 'userid' => $u1->id)); $this->setUser($u1); // Requesting a single letter. $return = mod_glossary_external::get_entries_by_letter($g1->id, 'b', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e1b->id, $return['entries'][0]['id']); // Requesting special letters. $return = mod_glossary_external::get_entries_by_letter($g1->id, 'SPECIAL', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); // Requesting with limit. $return = mod_glossary_external::get_entries_by_letter($g1->id, 'ALL', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); $return = mod_glossary_external::get_entries_by_letter($g1->id, 'ALL', 1, 2, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_letter_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1c->id, $return['entries'][0]['id']); $this->assertEquals($e1b->id, $return['entries'][1]['id']); } public function test_get_entries_by_date(): void { global $DB; $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $now = time(); $e1a = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Bob', 'userid' => $u1->id, 'timecreated' => 1, 'timemodified' => $now + 3600, 'tags' => array('Cats', 'Dogs'))); $e1b = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Jane', 'userid' => $u1->id, 'timecreated' => $now + 3600, 'timemodified' => 1)); $e1c = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Alice', 'userid' => $u1->id, 'timecreated' => $now + 1, 'timemodified' => $now + 1)); $e1d = $gg->create_content($g1, array('approved' => 0, 'concept' => '0-day', 'userid' => $u1->id, 'timecreated' => $now + 2, 'timemodified' => $now + 2)); $e2a = $gg->create_content($g2); $this->setAdminUser($u1); // Ordering by time modified descending. $return = mod_glossary_external::get_entries_by_date($g1->id, 'UPDATE', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); $this->assertEquals('Cats', $return['entries'][0]['tags'][0]['rawname']); $this->assertEquals('Dogs', $return['entries'][0]['tags'][1]['rawname']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1b->id, $return['entries'][2]['id']); // Ordering by time modified ascending. $return = mod_glossary_external::get_entries_by_date($g1->id, 'UPDATE', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1b->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1a->id, $return['entries'][2]['id']); // Ordering by time created asc. $return = mod_glossary_external::get_entries_by_date($g1->id, 'CREATION', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1b->id, $return['entries'][2]['id']); // Ordering by time created descending. $return = mod_glossary_external::get_entries_by_date($g1->id, 'CREATION', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1b->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1a->id, $return['entries'][2]['id']); // Ordering including to approve. $return = mod_glossary_external::get_entries_by_date($g1->id, 'CREATION', 'ASC', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1d->id, $return['entries'][2]['id']); $this->assertEquals($e1b->id, $return['entries'][3]['id']); // Ordering including to approve and pagination. $return = mod_glossary_external::get_entries_by_date($g1->id, 'CREATION', 'ASC', 0, 2, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $return = mod_glossary_external::get_entries_by_date($g1->id, 'CREATION', 'ASC', 2, 2, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_date_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); $this->assertEquals($e1b->id, $return['entries'][1]['id']); } public function test_get_categories(): void { $this->resetAfterTest(true); $this->setAdminUser(); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $cat1a = $gg->create_category($g1); $cat1b = $gg->create_category($g1); $cat1c = $gg->create_category($g1); $cat2a = $gg->create_category($g2); $return = mod_glossary_external::get_categories($g1->id, 0, 20); $return = external_api::clean_returnvalue(mod_glossary_external::get_categories_returns(), $return); $this->assertCount(3, $return['categories']); $this->assertEquals(3, $return['count']); $this->assertEquals($cat1a->id, $return['categories'][0]['id']); $this->assertEquals($cat1b->id, $return['categories'][1]['id']); $this->assertEquals($cat1c->id, $return['categories'][2]['id']); $return = mod_glossary_external::get_categories($g1->id, 1, 2); $return = external_api::clean_returnvalue(mod_glossary_external::get_categories_returns(), $return); $this->assertCount(2, $return['categories']); $this->assertEquals(3, $return['count']); $this->assertEquals($cat1b->id, $return['categories'][0]['id']); $this->assertEquals($cat1c->id, $return['categories'][1]['id']); } public function test_get_entries_by_category(): void { $this->resetAfterTest(true); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $e1a1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id, 'tags' => array('Cats', 'Dogs'))); $e1a2 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1a3 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1b1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1b2 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u1->id)); $e1x1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1x2 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u1->id)); $e2a1 = $gg->create_content($g2, array('approved' => 1, 'userid' => $u1->id)); $e2a2 = $gg->create_content($g2, array('approved' => 1, 'userid' => $u1->id)); $cat1a = $gg->create_category($g1, array('name' => 'Fish'), array($e1a1, $e1a2, $e1a3)); $cat1b = $gg->create_category($g1, array('name' => 'Cat'), array($e1b1, $e1b2)); $cat1c = $gg->create_category($g1, array('name' => 'Zebra'), array($e1b1)); // Entry $e1b1 is in two categories. $cat2a = $gg->create_category($g2, array(), array($e2a1, $e2a2)); $this->setAdminUser(); // Browse one category. $return = mod_glossary_external::get_entries_by_category($g1->id, $cat1a->id, 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_category_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a1->id, $return['entries'][0]['id']); $this->assertEquals('Cats', $return['entries'][0]['tags'][0]['rawname']); $this->assertEquals('Dogs', $return['entries'][0]['tags'][1]['rawname']); $this->assertEquals($e1a2->id, $return['entries'][1]['id']); $this->assertEquals($e1a3->id, $return['entries'][2]['id']); // Browse all categories. $return = mod_glossary_external::get_entries_by_category($g1->id, GLOSSARY_SHOW_ALL_CATEGORIES, 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_category_returns(), $return); $this->assertCount(5, $return['entries']); $this->assertEquals(5, $return['count']); $this->assertEquals($e1b1->id, $return['entries'][0]['id']); $this->assertEquals($e1a1->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); $this->assertEquals($e1a3->id, $return['entries'][3]['id']); $this->assertEquals($e1b1->id, $return['entries'][4]['id']); // Browse uncategorised. $return = mod_glossary_external::get_entries_by_category($g1->id, GLOSSARY_SHOW_NOT_CATEGORISED, 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_category_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e1x1->id, $return['entries'][0]['id']); // Including to approve. $return = mod_glossary_external::get_entries_by_category($g1->id, $cat1b->id, 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_category_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); $this->assertEquals($e1b1->id, $return['entries'][0]['id']); $this->assertEquals($e1b2->id, $return['entries'][1]['id']); // Using limit. $return = mod_glossary_external::get_entries_by_category($g1->id, GLOSSARY_SHOW_ALL_CATEGORIES, 0, 3, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_category_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1b1->id, $return['entries'][0]['id']); $this->assertEquals($e1b2->id, $return['entries'][1]['id']); $this->assertEquals($e1a1->id, $return['entries'][2]['id']); $return = mod_glossary_external::get_entries_by_category($g1->id, GLOSSARY_SHOW_ALL_CATEGORIES, 3, 2, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_category_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1a2->id, $return['entries'][0]['id']); $this->assertEquals($e1a3->id, $return['entries'][1]['id']); } public function test_get_authors(): void { $this->resetAfterTest(true); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(array('lastname' => 'Upsilon')); $u2 = $this->getDataGenerator()->create_user(array('lastname' => 'Alpha')); $u3 = $this->getDataGenerator()->create_user(array('lastname' => 'Omega')); $ctx = \context_module::instance($g1->cmid); $e1a = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 1)); $e1b = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 1)); $e1c = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 1)); $e2a = $gg->create_content($g1, array('userid' => $u2->id, 'approved' => 1)); $e3a = $gg->create_content($g1, array('userid' => $u3->id, 'approved' => 0)); $this->setAdminUser(); // Simple request. $return = mod_glossary_external::get_authors($g1->id, 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_authors_returns(), $return); $this->assertCount(2, $return['authors']); $this->assertEquals(2, $return['count']); $this->assertEquals($u2->id, $return['authors'][0]['id']); $this->assertEquals($u1->id, $return['authors'][1]['id']); // Include users with entries pending approval. $return = mod_glossary_external::get_authors($g1->id, 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_authors_returns(), $return); $this->assertCount(3, $return['authors']); $this->assertEquals(3, $return['count']); $this->assertEquals($u2->id, $return['authors'][0]['id']); $this->assertEquals($u3->id, $return['authors'][1]['id']); $this->assertEquals($u1->id, $return['authors'][2]['id']); // Pagination. $return = mod_glossary_external::get_authors($g1->id, 1, 1, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_authors_returns(), $return); $this->assertCount(1, $return['authors']); $this->assertEquals(3, $return['count']); $this->assertEquals($u3->id, $return['authors'][0]['id']); } public function test_get_entries_by_author(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $u1 = $this->getDataGenerator()->create_user(array('lastname' => 'Upsilon', 'firstname' => 'Zac')); $u2 = $this->getDataGenerator()->create_user(array('lastname' => 'Ultra', 'firstname' => '1337')); $u3 = $this->getDataGenerator()->create_user(array('lastname' => 'Alpha', 'firstname' => 'Omega')); $u4 = $this->getDataGenerator()->create_user(array('lastname' => '0-day', 'firstname' => 'Zoe')); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $e1a1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1a2 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1a3 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id)); $e1b1 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u2->id)); $e1b2 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u2->id, 'tags' => array('Cats', 'Dogs'))); $e1c1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u3->id)); $e1d1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u4->id)); $e2a = $gg->create_content($g2, array('approved' => 1, 'userid' => $u1->id)); $this->setUser($u1); // Requesting a single letter. $return = mod_glossary_external::get_entries_by_author($g1->id, 'u', 'LASTNAME', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1b2->id, $return['entries'][0]['id']); $this->assertEquals('Cats', $return['entries'][0]['tags'][0]['rawname']); $this->assertEquals('Dogs', $return['entries'][0]['tags'][1]['rawname']); $this->assertEquals($e1a1->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); $this->assertEquals($e1a3->id, $return['entries'][3]['id']); // Requesting special letters. $return = mod_glossary_external::get_entries_by_author($g1->id, 'SPECIAL', 'LASTNAME', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e1d1->id, $return['entries'][0]['id']); // Requesting with limit. $return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'LASTNAME', 'ASC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1d1->id, $return['entries'][0]['id']); $return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'LASTNAME', 'ASC', 1, 2, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1c1->id, $return['entries'][0]['id']); $this->assertEquals($e1b2->id, $return['entries'][1]['id']); // Including non-approved. $this->setAdminUser(); $return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'LASTNAME', 'ASC', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(7, $return['entries']); $this->assertEquals(7, $return['count']); $this->assertEquals($e1d1->id, $return['entries'][0]['id']); $this->assertEquals($e1c1->id, $return['entries'][1]['id']); $this->assertEquals($e1b1->id, $return['entries'][2]['id']); $this->assertEquals($e1b2->id, $return['entries'][3]['id']); $this->assertEquals($e1a1->id, $return['entries'][4]['id']); $this->assertEquals($e1a2->id, $return['entries'][5]['id']); $this->assertEquals($e1a3->id, $return['entries'][6]['id']); // Changing order. $return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'LASTNAME', 'DESC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1a1->id, $return['entries'][0]['id']); // Sorting by firstname. $return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'FIRSTNAME', 'ASC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1b2->id, $return['entries'][0]['id']); // Sorting by firstname descending. $return = mod_glossary_external::get_entries_by_author($g1->id, 'ALL', 'FIRSTNAME', 'DESC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(6, $return['count']); $this->assertEquals($e1d1->id, $return['entries'][0]['id']); // Filtering by firstname descending. $return = mod_glossary_external::get_entries_by_author($g1->id, 'z', 'FIRSTNAME', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1d1->id, $return['entries'][0]['id']); $this->assertEquals($e1a1->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); $this->assertEquals($e1a3->id, $return['entries'][3]['id']); // Test with a deleted user. delete_user($u2); $return = mod_glossary_external::get_entries_by_author($g1->id, 'u', 'LASTNAME', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1b2->id, $return['entries'][0]['id']); $this->assertEquals($e1a1->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); $this->assertEquals($e1a3->id, $return['entries'][3]['id']); } public function test_get_entries_by_author_id(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id, 'displayformat' => 'entrylist')); $u1 = $this->getDataGenerator()->create_user(array('lastname' => 'Upsilon', 'firstname' => 'Zac')); $u2 = $this->getDataGenerator()->create_user(array('lastname' => 'Ultra', 'firstname' => '1337')); $u3 = $this->getDataGenerator()->create_user(array('lastname' => 'Alpha', 'firstname' => 'Omega')); $u4 = $this->getDataGenerator()->create_user(array('lastname' => '0-day', 'firstname' => 'Zoe')); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $e1a1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id, 'concept' => 'Zoom', 'timecreated' => 3600, 'timemodified' => time() - 3600)); $e1a2 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id, 'concept' => 'Alpha')); $e1a3 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id, 'concept' => 'Dog', 'timecreated' => 1, 'timemodified' => time() - 1800)); $e1a4 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u1->id, 'concept' => 'Bird')); $e1b1 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u2->id)); $e2a = $gg->create_content($g2, array('approved' => 1, 'userid' => $u1->id)); $this->setAdminUser(); // Standard request. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CONCEPT', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a2->id, $return['entries'][0]['id']); $this->assertEquals($e1a3->id, $return['entries'][1]['id']); $this->assertEquals($e1a1->id, $return['entries'][2]['id']); // Standard request descending. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CONCEPT', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a1->id, $return['entries'][0]['id']); $this->assertEquals($e1a3->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); // Requesting ordering by time created. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CREATION', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a3->id, $return['entries'][0]['id']); $this->assertEquals($e1a1->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); // Requesting ordering by time created descending. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CREATION', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a2->id, $return['entries'][0]['id']); $this->assertEquals($e1a1->id, $return['entries'][1]['id']); $this->assertEquals($e1a3->id, $return['entries'][2]['id']); // Requesting ordering by time modified. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'UPDATE', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a1->id, $return['entries'][0]['id']); $this->assertEquals($e1a3->id, $return['entries'][1]['id']); $this->assertEquals($e1a2->id, $return['entries'][2]['id']); // Requesting ordering by time modified descending. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'UPDATE', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1a2->id, $return['entries'][0]['id']); $this->assertEquals($e1a3->id, $return['entries'][1]['id']); $this->assertEquals($e1a1->id, $return['entries'][2]['id']); // Including non approved. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CONCEPT', 'ASC', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1a2->id, $return['entries'][0]['id']); $this->assertEquals($e1a4->id, $return['entries'][1]['id']); $this->assertEquals($e1a3->id, $return['entries'][2]['id']); $this->assertEquals($e1a1->id, $return['entries'][3]['id']); // Pagination. $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CONCEPT', 'ASC', 0, 2, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1a2->id, $return['entries'][0]['id']); $this->assertEquals($e1a4->id, $return['entries'][1]['id']); $return = mod_glossary_external::get_entries_by_author_id($g1->id, $u1->id, 'CONCEPT', 'ASC', 1, 2, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_author_id_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1a4->id, $return['entries'][0]['id']); $this->assertEquals($e1a3->id, $return['entries'][1]['id']); } public function test_get_entries_by_search(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $this->setUser($u1); $e1 = $gg->create_content($g1, array('approved' => 1, 'concept' => 'House', 'timecreated' => time() + 3600)); $e2 = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Mouse', 'timemodified' => 1)); $e3 = $gg->create_content($g1, array('approved' => 1, 'concept' => 'Hero', 'tags' => array('Cats', 'Dogs'))); $e4 = $gg->create_content($g1, array('approved' => 0, 'concept' => 'Toulouse')); $e5 = $gg->create_content($g1, array('approved' => 1, 'definition' => 'Heroes', 'concept' => 'Abcd')); $e6 = $gg->create_content($g1, array('approved' => 0, 'definition' => 'When used for Heroes')); $e7 = $gg->create_content($g1, array('approved' => 1, 'timecreated' => 1, 'timemodified' => time() + 3600, 'concept' => 'Z'), array('Couscous')); $e8 = $gg->create_content($g1, array('approved' => 0), array('Heroes')); $e9 = $gg->create_content($g2, array('approved' => 0)); $this->setAdminUser(); // Test simple query. $query = 'hero'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'CONCEPT', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e3->id, $return['entries'][0]['id']); $this->assertEquals('Cats', $return['entries'][0]['tags'][0]['rawname']); $this->assertEquals('Dogs', $return['entries'][0]['tags'][1]['rawname']); // Enabling full search. $query = 'hero'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, true, 'CONCEPT', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); $this->assertEquals($e5->id, $return['entries'][0]['id']); $this->assertEquals($e3->id, $return['entries'][1]['id']); // Concept descending. $query = 'hero'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, true, 'CONCEPT', 'DESC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); $this->assertEquals($e3->id, $return['entries'][0]['id']); $this->assertEquals($e5->id, $return['entries'][1]['id']); // Search on alias. $query = 'couscous'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'CONCEPT', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e7->id, $return['entries'][0]['id']); $return = mod_glossary_external::get_entries_by_search($g1->id, $query, true, 'CONCEPT', 'ASC', 0, 20, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e7->id, $return['entries'][0]['id']); // Pagination and ordering on created date. $query = 'ou'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'CREATION', 'ASC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e7->id, $return['entries'][0]['id']); $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'CREATION', 'DESC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e1->id, $return['entries'][0]['id']); // Ordering on updated date. $query = 'ou'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'UPDATE', 'ASC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e2->id, $return['entries'][0]['id']); $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'UPDATE', 'DESC', 0, 1, array()); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); $this->assertEquals($e7->id, $return['entries'][0]['id']); // Including not approved. $query = 'ou'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, false, 'CONCEPT', 'ASC', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1->id, $return['entries'][0]['id']); $this->assertEquals($e2->id, $return['entries'][1]['id']); $this->assertEquals($e4->id, $return['entries'][2]['id']); $this->assertEquals($e7->id, $return['entries'][3]['id']); // Advanced query string. $query = '+Heroes -Abcd'; $return = mod_glossary_external::get_entries_by_search($g1->id, $query, true, 'CONCEPT', 'ASC', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_search_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); $this->assertEquals($e6->id, $return['entries'][0]['id']); $this->assertEquals($e8->id, $return['entries'][1]['id']); } public function test_get_entries_by_term(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $this->setAdminUser(); $e1 = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 1, 'concept' => 'cat', 'tags' => array('Cats', 'Dogs'))); $e2 = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 1), array('cat', 'dog')); $e3 = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 1), array('dog')); $e4 = $gg->create_content($g1, array('userid' => $u1->id, 'approved' => 0, 'concept' => 'dog')); $e5 = $gg->create_content($g2, array('userid' => $u1->id, 'approved' => 1, 'concept' => 'dog'), array('cat')); // Search concept + alias. $return = mod_glossary_external::get_entries_by_term($g1->id, 'cat', 0, 20, ['includenotapproved' => false]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); // Compare ids, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = array($e1->id, $e2->id); $actual = array($return['entries'][0]['id'], $return['entries'][1]['id']); $this->assertEqualsCanonicalizing($expected, $actual); // Compare rawnames of all expected tags, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = array('Cats', 'Dogs'); // Only $e1 has 2 tags. $actual = array(); // Accumulate all tags returned. foreach ($return['entries'] as $entry) { foreach ($entry['tags'] as $tag) { $actual[] = $tag['rawname']; } } $this->assertEqualsCanonicalizing($expected, $actual); // Search alias. $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 20, ['includenotapproved' => false]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); // Compare ids, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = array($e2->id, $e3->id); $actual = array($return['entries'][0]['id'], $return['entries'][1]['id']); $this->assertEqualsCanonicalizing($expected, $actual); // Search including not approved. $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); // Compare ids, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = array($e4->id, $e2->id, $e3->id); $actual = array($return['entries'][0]['id'], $return['entries'][1]['id'], $return['entries'][2]['id']); $this->assertEqualsCanonicalizing($expected, $actual); // Pagination. $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 1, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(1, $return['entries']); // We don't compare the returned entry id because it may be different depending on the DBMS, // for example, Postgres does a random sorting in this case. $this->assertEquals(3, $return['count']); $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 1, 1, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); } /** * Test get_entries_by_multilingual_term. * * @covers \mod_glossary_external::get_entries_by_term * @return void * @throws \coding_exception * @throws \invalid_response_exception * @throws \moodle_exception */ public function test_get_entries_by_multilingual_term(): void { $this->resetAfterTest(true); // Enable multilang filter to on content and heading. filter_set_global_state('multilang', TEXTFILTER_ON); filter_set_applies_to_strings('multilang', 1); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', ['course' => $c1->id]); $g2 = $this->getDataGenerator()->create_module('glossary', ['course' => $c1->id]); $u1 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $this->setAdminUser(); $e1 = $gg->create_content($g1, ['userid' => $u1->id, 'approved' => 1, 'concept' => 'catchat', 'tags' => ['Cats', 'Dogs']]); $e2 = $gg->create_content($g1, ['userid' => $u1->id, 'approved' => 1], ['catchat', '' . 'dogchien']); $e3 = $gg->create_content($g1, ['userid' => $u1->id, 'approved' => 1], ['dogchien']); $e4 = $gg->create_content($g1, ['userid' => $u1->id, 'approved' => 0, 'concept' => 'dogchien']); $e5 = $gg->create_content($g2, ['userid' => $u1->id, 'approved' => 1, 'concept' => 'cogchien'], ['cat']); // Search concept + alias. $return = mod_glossary_external::get_entries_by_term($g1->id, 'cat', 0, 20, ['includenotapproved' => false]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); // Compare ids, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = [$e1->id, $e2->id]; $actual = [$return['entries'][0]['id'], $return['entries'][1]['id']]; $this->assertEqualsCanonicalizing($expected, $actual); // Compare rawnames of all expected tags, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = ['Cats', 'Dogs']; // Only $e1 has 2 tags. $actual = []; // Accumulate all tags returned. foreach ($return['entries'] as $entry) { foreach ($entry['tags'] as $tag) { $actual[] = $tag['rawname']; } } $this->assertEqualsCanonicalizing($expected, $actual); // Search alias. $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 20, ['includenotapproved' => false]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(2, $return['count']); // Compare ids, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = [$e2->id, $e3->id]; $actual = [$return['entries'][0]['id'], $return['entries'][1]['id']]; $this->assertEqualsCanonicalizing($expected, $actual); // Search including not approved. $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 20, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(3, $return['entries']); $this->assertEquals(3, $return['count']); // Compare ids, ignore ordering of array, using canonicalize parameter of assertEquals. $expected = [$e4->id, $e2->id, $e3->id]; $actual = [$return['entries'][0]['id'], $return['entries'][1]['id'], $return['entries'][2]['id']]; $this->assertEqualsCanonicalizing($expected, $actual); // Pagination. $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 0, 1, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(1, $return['entries']); // We don't compare the returned entry id because it may be different depending on the DBMS, // for example, Postgres does a random sorting in this case. $this->assertEquals(3, $return['count']); $return = mod_glossary_external::get_entries_by_term($g1->id, 'dog', 1, 1, ['includenotapproved' => true]); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_by_term_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(3, $return['count']); } public function test_get_entries_to_approve(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $u1 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $e1a = $gg->create_content($g1, array('approved' => 0, 'concept' => 'Bob', 'userid' => $u1->id, 'timecreated' => time() + 3600)); $e1b = $gg->create_content($g1, array('approved' => 0, 'concept' => 'Jane', 'userid' => $u1->id, 'timecreated' => 1)); $e1c = $gg->create_content($g1, array('approved' => 0, 'concept' => 'Alice', 'userid' => $u1->id, 'timemodified' => 1)); $e1d = $gg->create_content($g1, array('approved' => 0, 'concept' => '0-day', 'userid' => $u1->id, 'timemodified' => time() + 3600)); $e1e = $gg->create_content($g1, array('approved' => 1, 'concept' => '1-day', 'userid' => $u1->id)); $e2a = $gg->create_content($g2); $this->setAdminUser(true); // Simple listing. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CONCEPT', 'ASC', 0, 20); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $this->assertEquals($e1a->id, $return['entries'][2]['id']); $this->assertEquals($e1b->id, $return['entries'][3]['id']); // Revert ordering of concept. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CONCEPT', 'DESC', 0, 20); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(4, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1b->id, $return['entries'][0]['id']); $this->assertEquals($e1a->id, $return['entries'][1]['id']); $this->assertEquals($e1c->id, $return['entries'][2]['id']); $this->assertEquals($e1d->id, $return['entries'][3]['id']); // Filtering by letter. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'a', 'CONCEPT', 'ASC', 0, 20); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e1c->id, $return['entries'][0]['id']); // Filtering by special. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'SPECIAL', 'CONCEPT', 'ASC', 0, 20); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(1, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); // Pagination. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CONCEPT', 'ASC', 0, 2); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); $this->assertEquals($e1c->id, $return['entries'][1]['id']); $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CONCEPT', 'ASC', 1, 2); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(2, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1c->id, $return['entries'][0]['id']); $this->assertEquals($e1a->id, $return['entries'][1]['id']); // Ordering by creation date. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CREATION', 'ASC', 0, 1); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1b->id, $return['entries'][0]['id']); // Ordering by creation date desc. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CREATION', 'DESC', 0, 1); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1a->id, $return['entries'][0]['id']); // Ordering by update date. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'UPDATE', 'ASC', 0, 1); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1c->id, $return['entries'][0]['id']); // Ordering by update date desc. $return = mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'UPDATE', 'DESC', 0, 1); $return = external_api::clean_returnvalue(mod_glossary_external::get_entries_to_approve_returns(), $return); $this->assertCount(1, $return['entries']); $this->assertEquals(4, $return['count']); $this->assertEquals($e1d->id, $return['entries'][0]['id']); // Permissions are checked. $this->setUser($u1); $this->expectException('required_capability_exception'); mod_glossary_external::get_entries_to_approve($g1->id, 'ALL', 'CONCEPT', 'ASC', 0, 1); $this->fail('Do not test anything else after this.'); } public function test_get_entry_by_id(): void { $this->resetAfterTest(true); // Generate all the things. $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $c1 = $this->getDataGenerator()->create_course(); $c2 = $this->getDataGenerator()->create_course(); $g1 = $this->getDataGenerator()->create_module('glossary', array('course' => $c1->id)); $g2 = $this->getDataGenerator()->create_module('glossary', array('course' => $c2->id, 'visible' => 0)); $u1 = $this->getDataGenerator()->create_user(); $u2 = $this->getDataGenerator()->create_user(); $u3 = $this->getDataGenerator()->create_user(); $ctx = \context_module::instance($g1->cmid); $this->getDataGenerator()->enrol_user($u1->id, $c1->id); $this->getDataGenerator()->enrol_user($u2->id, $c1->id); $this->getDataGenerator()->enrol_user($u3->id, $c1->id); $e1 = $gg->create_content($g1, array('approved' => 1, 'userid' => $u1->id, 'tags' => array('Cats', 'Dogs'))); // Add a fake inline image to the entry. $filename = 'shouldbeanimage.jpg'; $filerecordinline = array( 'contextid' => $ctx->id, 'component' => 'mod_glossary', 'filearea' => 'entry', 'itemid' => $e1->id, 'filepath' => '/', 'filename' => $filename, ); $fs = get_file_storage(); $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); $e2 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u1->id)); $e3 = $gg->create_content($g1, array('approved' => 0, 'userid' => $u2->id)); $e4 = $gg->create_content($g2, array('approved' => 1)); $this->setUser($u1); $return = mod_glossary_external::get_entry_by_id($e1->id); $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return); $this->assertEquals($e1->id, $return['entry']['id']); $this->assertEquals('Cats', $return['entry']['tags'][0]['rawname']); $this->assertEquals('Dogs', $return['entry']['tags'][1]['rawname']); $this->assertEquals($filename, $return['entry']['definitioninlinefiles'][0]['filename']); $this->assertTrue($return['permissions']['candelete']); $return = mod_glossary_external::get_entry_by_id($e2->id); $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return); $this->assertEquals($e2->id, $return['entry']['id']); $this->assertTrue($return['permissions']['candelete']); try { $return = mod_glossary_external::get_entry_by_id($e3->id); $this->fail('Cannot view unapproved entries of others.'); } catch (\invalid_parameter_exception $e) { // All good. } try { $return = mod_glossary_external::get_entry_by_id($e4->id); $this->fail('Cannot view entries from another course.'); } catch (\require_login_exception $e) { // All good. } // An admin can see other's entries to be approved. $this->setAdminUser(); $return = mod_glossary_external::get_entry_by_id($e3->id); $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return); $this->assertEquals($e3->id, $return['entry']['id']); $this->assertTrue($return['permissions']['candelete']); // Students can see other students approved entries but they will not be able to delete them. $this->setUser($u3); $return = mod_glossary_external::get_entry_by_id($e1->id); $return = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $return); $this->assertEquals($e1->id, $return['entry']['id']); $this->assertFalse($return['permissions']['candelete']); } public function test_add_entry_without_optional_settings(): void { global $CFG, $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); $this->setAdminUser(); $concept = 'A concept'; $definition = '

A definition

'; $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); // Get entry from DB. $entry = $DB->get_record('glossary_entries', array('id' => $return['entryid'])); $this->assertEquals($concept, $entry->concept); $this->assertEquals($definition, $entry->definition); $this->assertEquals($CFG->glossary_linkentries, $entry->usedynalink); $this->assertEquals($CFG->glossary_casesensitive, $entry->casesensitive); $this->assertEquals($CFG->glossary_fullmatch, $entry->fullmatch); $this->assertEmpty($DB->get_records('glossary_alias', array('entryid' => $return['entryid']))); $this->assertEmpty($DB->get_records('glossary_entries_categories', array('entryid' => $return['entryid']))); } public function test_add_entry_with_aliases(): void { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); $this->setAdminUser(); $concept = 'A concept'; $definition = 'A definition'; $paramaliases = 'abc, def, gez'; $options = array( array( 'name' => 'aliases', 'value' => $paramaliases, ) ); $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); $aliases = $DB->get_records('glossary_alias', array('entryid' => $return['entryid'])); $this->assertCount(3, $aliases); foreach ($aliases as $alias) { $this->assertStringContainsString($alias->alias, $paramaliases); } } public function test_add_entry_in_categories(): void { global $DB; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $cat1 = $gg->create_category($glossary); $cat2 = $gg->create_category($glossary); $this->setAdminUser(); $concept = 'A concept'; $definition = 'A definition'; $paramcategories = "$cat1->id, $cat2->id"; $options = array( array( 'name' => 'categories', 'value' => $paramcategories, ) ); $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); $categories = $DB->get_records('glossary_entries_categories', array('entryid' => $return['entryid'])); $this->assertCount(2, $categories); foreach ($categories as $category) { $this->assertStringContainsString($category->categoryid, $paramcategories); } } public function test_add_entry_with_attachments(): void { global $DB, $USER; $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $glossary = $this->getDataGenerator()->create_module('glossary', array('course' => $course->id)); $context = \context_module::instance($glossary->cmid); $this->setAdminUser(); $concept = 'A concept'; $definition = 'A definition'; // Draft files. $draftidinlineattach = file_get_unused_draft_itemid(); $draftidattach = file_get_unused_draft_itemid(); $usercontext = \context_user::instance($USER->id); $filerecordinline = array( 'contextid' => $usercontext->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => $draftidinlineattach, 'filepath' => '/', 'filename' => 'shouldbeanimage.txt', ); $fs = get_file_storage(); // Create a file in a draft area for regular attachments. $filerecordattach = $filerecordinline; $attachfilename = 'attachment.txt'; $filerecordattach['filename'] = $attachfilename; $filerecordattach['itemid'] = $draftidattach; $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); $fs->create_file_from_string($filerecordattach, 'simple text attachment'); $options = array( array( 'name' => 'inlineattachmentsid', 'value' => $draftidinlineattach, ), array( 'name' => 'attachmentsid', 'value' => $draftidattach, ) ); $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML, $options); $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return); $editorfiles = external_util::get_area_files($context->id, 'mod_glossary', 'entry', $return['entryid']); $attachmentfiles = external_util::get_area_files($context->id, 'mod_glossary', 'attachment', $return['entryid']); $this->assertCount(1, $editorfiles); $this->assertCount(1, $attachmentfiles); $this->assertEquals('shouldbeanimage.txt', $editorfiles[0]['filename']); $this->assertEquals('attachment.txt', $attachmentfiles[0]['filename']); } /** * Test get entry including rating information. */ public function test_get_entry_rating_information(): void { $this->resetAfterTest(true); global $DB, $CFG; require_once($CFG->dirroot . '/rating/lib.php'); $this->resetAfterTest(true); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); $teacher = self::getDataGenerator()->create_user(); // Create course to add the module. $course = self::getDataGenerator()->create_course(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($user1->id, $course->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($user3->id, $course->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual'); // Create the glossary and contents. $record = new \stdClass(); $record->course = $course->id; $record->assessed = RATING_AGGREGATE_AVERAGE; $scale = $this->getDataGenerator()->create_scale(array('scale' => 'A,B,C,D')); $record->scale = "-$scale->id"; $glossary = $this->getDataGenerator()->create_module('glossary', $record); $context = \context_module::instance($glossary->cmid); $gg = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $entry = $gg->create_content($glossary, array('approved' => 1, 'userid' => $user1->id)); // Rate the entry as user2. $rating1 = new \stdClass(); $rating1->contextid = $context->id; $rating1->component = 'mod_glossary'; $rating1->ratingarea = 'entry'; $rating1->itemid = $entry->id; $rating1->rating = 1; // 1 is A. $rating1->scaleid = "-$scale->id"; $rating1->userid = $user2->id; $rating1->timecreated = time(); $rating1->timemodified = time(); $rating1->id = $DB->insert_record('rating', $rating1); // Rate the entry as user3. $rating2 = new \stdClass(); $rating2->contextid = $context->id; $rating2->component = 'mod_glossary'; $rating2->ratingarea = 'entry'; $rating2->itemid = $entry->id; $rating2->rating = 3; // 3 is C. $rating2->scaleid = "-$scale->id"; $rating2->userid = $user3->id; $rating2->timecreated = time() + 1; $rating2->timemodified = time() + 1; $rating2->id = $DB->insert_record('rating', $rating2); // As student, retrieve ratings information. $this->setUser($user1); $result = mod_glossary_external::get_entry_by_id($entry->id); $result = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $result); $this->assertCount(1, $result['ratinginfo']['ratings']); $this->assertFalse($result['ratinginfo']['ratings'][0]['canviewaggregate']); $this->assertFalse($result['ratinginfo']['canviewall']); $this->assertFalse($result['ratinginfo']['ratings'][0]['canrate']); $this->assertTrue(!isset($result['ratinginfo']['ratings'][0]['count'])); // Now, as teacher, I should see the info correctly. $this->setUser($teacher); $result = mod_glossary_external::get_entry_by_id($entry->id); $result = external_api::clean_returnvalue(mod_glossary_external::get_entry_by_id_returns(), $result); $this->assertCount(1, $result['ratinginfo']['ratings']); $this->assertTrue($result['ratinginfo']['ratings'][0]['canviewaggregate']); $this->assertTrue($result['ratinginfo']['canviewall']); $this->assertTrue($result['ratinginfo']['ratings'][0]['canrate']); $this->assertEquals(2, $result['ratinginfo']['ratings'][0]['count']); $this->assertEquals(2, $result['ratinginfo']['ratings'][0]['aggregate']); // 2 is B, that is the average of A + C. } } behat/login.php000064400000005735152265517610007474 0ustar00. // phpcs:disable moodle.Files.RequireLogin.Missing // phpcs:disable moodle.PHP.ForbiddenFunctions.Found /** * Login end point for Behat tests only. * * @package core_auth * @category test * @author Guy Thomas * @copyright 2021 Class Technologies Inc. {@link https://www.class.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../../config.php'); require_once("{$CFG->dirroot}/login/lib.php"); $behatrunning = defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING; if (!$behatrunning) { redirect(new moodle_url('/')); } $username = required_param('username', PARAM_ALPHANUMEXT); $wantsurl = optional_param('wantsurl', null, PARAM_URL); if (isloggedin()) { // If the user is already logged in, log them out and redirect them back to login again. require_logout(); redirect(new moodle_url('/auth/tests/behat/login.php', [ 'username' => $username, 'wantsurl' => (new moodle_url($wantsurl))->out(false), ])); } // Note - with behat, the password is always the same as the username. $password = $username; $failurereason = null; $user = authenticate_user_login($username, $password, true, $failurereason, false); if ($failurereason) { switch($failurereason) { case AUTH_LOGIN_NOUSER: $reason = get_string('invalidlogin'); break; case AUTH_LOGIN_SUSPENDED: $reason = 'User suspended'; break; case AUTH_LOGIN_FAILED: $reason = 'Login failed'; break; case AUTH_LOGIN_LOCKOUT: $reason = 'Account locked'; break; case AUTH_LOGIN_UNAUTHORISED: $reason = get_string('unauthorisedlogin', 'core', $username); break; default: $reason = "Unknown login failure: '{$failurereason}'"; break; } // Note: Do not throw an exception here as we sometimes test that login does not work. // Exceptions are automatic failures in Behat. \core\notification::add($reason, \core\notification::ERROR); redirect(new moodle_url('/')); } if (!complete_user_login($user)) { throw new Exception("Failed to login as behat step for $username"); } if (empty($wantsurl)) { $wantsurl = core_login_get_return_url(); } redirect(new moodle_url($wantsurl)); behat/loginform.feature000064400000011423152265517610011213 0ustar00@auth @core_auth @javascript Feature: Test if the login form provides the correct feedback In order to check if the login form provides correct feedback As a user I need to go on login page and see feedback on incorrect username or password. Background: Given the following "users" exist: | username | | teacher1 | Scenario: Check invalid login message Given I follow "Log in" And I set the field "Username" to "teacher1" And I set the field "Password" to "incorrect" When I press "Log in" Then I should see "Invalid login, please try again" Scenario: Test login language selector Given remote langimport tests are enabled And the following "language packs" exist: | language | | nl | | es | And the following config values are set as admin: | langmenu | 1 | And I follow "Log in" And I open the action menu in "region-main" "region" # The line below contains the unicode character U+200E before and after the brackets, please be very careful editing this line. When I choose "Nederlands ‎(nl)‎" in the open action menu Then I should see "Gebruikersnaam" @_file_upload Scenario: Set logo for loginpage Given I log in as "admin" And I navigate to "Appearance > Logos" in site administration And I upload "course/tests/fixtures/image.jpg" file to "Logo" filemanager And I press "Save changes" And I log out And I follow "Log in" Then "//img[@id='logoimage']" "xpath_element" should exist Scenario: Add a custom welcome message Given the following config values are set as admin: | auth_instructions | Lorem ipsum dolor sit amet | And I follow "Log in" Then I should see "Lorem ipsum dolor sit amet" @javascript @accessibility Scenario: Show the maintenance mode message Given the following config values are set as admin: | maintenance_enabled | 1 | | maintenance_message | Back online tomorrow | And I follow "Log in" Then I should see "Back online tomorrow" And the page should meet accessibility standards with "best-practice" extra tests Scenario: User self registration Given the following config values are set as admin: | registerauth | Email-based self-registration | And I follow "Log in" Then I should see "Create new account" Scenario: Set OAuth providers Given I log in as "admin" And I navigate to "Plugins > Authentication > Manage authentication" in site administration And I click on "Enable" "link" in the "OAuth 2" "table_row" And I navigate to "Server > OAuth 2 services" in site administration And I press "Google" And I set the field "Client ID" to "1234" And I set the field "Client secret" to "1234" And I press "Save changes" And I press "Facebook" And I set the field "Client ID" to "1234" And I set the field "Client secret" to "1234" And I press "Save changes" And I press "Microsoft" And I set the field "Client ID" to "1234" And I set the field "Client secret" to "1234" And I press "Save changes" And I log out And I follow "Log in" Then I should see "Google" And I should see "Facebook" And I should see "Microsoft" Scenario: Test the login page auto focus feature Given the following config values are set as admin: | loginpageautofocus | Enabled | And I follow "Log in" Then the focused element is "Username" "field" And I set the field "Username" to "admin" And I set the field "Password" to "admin" And I press "Log in" And I log out And I follow "Log in" Then the focused element is "Password" "field" @accessibility Scenario: Test the login page focus after error feature Given I follow "Log in" And I set the field "Username" to "admin" And I set the field "Password" to "wrongpassword" And I press "Log in" And I wait until the page is ready Then the focused element is "Username" "field" And the page should meet accessibility standards with "best-practice" extra tests Scenario: Display the password visibility toggle icon Given the following config values are set as admin: | loginpasswordtoggle | 1 | When I follow "Log in" Then "Toggle sensitive" "button" should be visible And the following config values are set as admin: | loginpasswordtoggle | 0 | And I reload the page And "Toggle sensitive" "button" should not be visible Scenario: Display the password visibility toggle icon for small screens only Given the following config values are set as admin: | loginpasswordtoggle | 2 | When I follow "Log in" Then "Toggle sensitive" "button" should not be visible And I change the viewport size to "mobile" And "Toggle sensitive" "button" should be visible behat/rememberusername.feature000064400000004477152265517610012570 0ustar00@core @core_auth Feature: Test the 'remember username' feature works. In order for users to easily log in to the site As a user I need the site to remember my username when the feature is enabled Background: Given the following "users" exist: | username | | teacher1 | # Given the user has logged in and selected 'Remember username', when they log in again, then their username should be remembered. Scenario: Check that 'remember username' works without javascript for teachers. # Log in the first time with $CFG->rememberusername set to Yes. Given the following config values are set as admin: | rememberusername | 1 | And I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" And I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" And I press "Log in" And I log out # Log out and check that the username was remembered. When I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" Then the field "username" matches value "teacher1" # Given the user has logged in before and selected 'Remember username', when they log in again and unset 'Remember username', then # their username should be forgotten for future log in attempts. Scenario: Check that 'remember username' unsetting works without javascript for teachers. # Log in the first time with $CFG->rememberusername set to Optional. Given the following config values are set as admin: | rememberusername | 2 | And I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" And I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" And I press "Log in" And I log out # Log in again, the username should have been remembered. When I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" Then the field "username" matches value "teacher1" And I set the field "Password" to "teacher1" And I press "Log in" And I log out And the following config values are set as admin: | rememberusername | 0 | # Check username has been forgotten. And I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" Then the field "username" matches value "" behat/logout.php000064400000002523152265517610007665 0ustar00. // phpcs:disable moodle.Files.RequireLogin.Missing // phpcs:disable moodle.PHP.ForbiddenFunctions.Found /** * Login end point for Behat tests only. * * @package core_auth * @category test * @copyright Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../../config.php'); $behatrunning = defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING; if (!$behatrunning) { redirect(new moodle_url('/login/logout.php')); } require_logout(); $login = optional_param('loginpage', 0, PARAM_BOOL); if ($login) { redirect(get_login_url()); } else { redirect(new moodle_url('/')); } behat/behat_auth.php000064400000005000152265517610010451 0ustar00. /** * Basic authentication steps definitions. * * @package core_auth * @category test * @copyright 2012 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); /** * Log in log out steps definitions. * * @package core_auth * @category test * @copyright 2012 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_auth extends behat_base { /** * Logs in the user. There should exist a user with the same value as username and password. * * @Given /^I log in as "(?P(?:[^"]|\\")*)"$/ * @Given I am logged in as :username * @param string $username the user to log in as. * @param moodle_url|null $wantsurl optional, URL to go to after logging in. */ public function i_log_in_as(string $username, ?moodle_url $wantsurl = null) { // In the mobile app the required tasks are different (does not support $wantsurl). if ($this->is_in_app()) { $this->execute('behat_app::login', [$username]); return; } $loginurl = new moodle_url('/auth/tests/behat/login.php', [ 'username' => $username, ]); if ($wantsurl !== null) { $loginurl->param('wantsurl', $wantsurl->out_as_local_url()); } // Visit login page. $this->execute('behat_general::i_visit', [$loginurl]); } /** * Logs out of the system. * * @Given /^I log out$/ * @Given I am not logged in */ public function i_log_out() { $this->execute('behat_general::i_visit', [new moodle_url('/auth/tests/behat/logout.php')]); } } behat/login.feature000064400000006202152265517610010326 0ustar00@core @core_auth Feature: Authentication In order to validate my credentials in the system As a user I need to log into the system Scenario: Log in with the predefined admin user with Javascript disabled Given I log in as "admin" Then I should see "You are logged in as Admin User" in the "page-footer" "region" @javascript Scenario: Log in with the predefined admin user with Javascript enabled Given I log in as "admin" Then I should see "You are logged in as Admin User" in the "page-footer" "region" Scenario: Log in as an existing admin user filling the form Given the following "users" exist: | username | password | firstname | lastname | email | | testuser | testuser | Test | User | moodle@example.com | And I am on site homepage When I follow "Log in" And I set the field "Username" to "testuser" And I set the field "Password" to "testuser" And I press "Log in" Then I should see "You are logged in as" in the "page-footer" "region" Scenario: Log in as an unexisting user filling the form Given the following "users" exist: | username | password | firstname | lastname | email | | testuser | testuser | Test | User | moodle@example.com | And I am on site homepage When I follow "Log in" And I set the field "Username" to "testuser" And I set the field "Password" to "unexisting" And I press "Log in" Then I should see "Invalid login, please try again" Scenario: Log out using the Log out link Given I log in as "admin" When I click on "Log out" "link" in the "#page-footer" "css_element" Then I should see "You are not logged in" in the "page-footer" "region" @javascript @accessibility Scenario: Login page must be accessible When I am on site homepage # The following tests are all provided to ensure that the accessibility tests themselves are tested. # In normal tests only one of the following is required. Then the page should meet accessibility standards And the page should meet "wcag131, wcag141, wcag412" accessibility standards And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests And I follow "Log in" And the page should meet accessibility standards And the page should meet "wcag131, wcag141, wcag412" accessibility standards And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests @javascript @accessibility Scenario: The login page must meet accessibility standards Given the following config values are set as admin: | custommenuitems | -This is a custom item\|/customurl/ | When I am on site homepage Then the page should meet accessibility standards with "best-practice" extra tests And I follow "Log in" And the page should meet accessibility standards with "best-practice" extra tests Scenario: Alternate login URL can be bypassed Given the following config values are set as admin: | alternateloginurl | https://www.google.com/ | And I am on site homepage When I visit "/login/index.php?loginredirect=0" Then I should see "Log in to Acceptance test site" behat/verifyageofconsent.feature000064400000003775152265517610013132 0ustar00@core @verify_age_location Feature: Test the 'Digital age of consent verification' feature works. In order to self-register on the site As an user I need be to be over the age of digital consent Background: Given the following config values are set as admin: | registerauth | email | | agedigitalconsentverification | 1 | Scenario: User that is not considered a digital minor attempts to self-register on the site. # Try to access the sign up page. Given I am on homepage When I click on "Log in" "link" in the ".logininfo" "css_element" And I click on "Create new account" "link" Then I should see "Age and location verification" When I set the field "What is your age?" to "16" And I set the field "In which country do you live?" to "DZ" And I press "Proceed" Then I should see "New account" And I should see "Username" # Try to access the sign up page again. When I press "Cancel" And I click on "Create new account" "link" Then I should see "New account" And I should see "Username" Scenario: User that is considered a digital minor attempts to self-register on the site. # Try to access the sign up page. Given I am on homepage When I click on "Log in" "link" in the ".logininfo" "css_element" And I click on "Create new account" "link" Then I should see "Age and location verification" When I set the field "What is your age?" to "12" And I set the field "In which country do you live?" to "AT" And I press "Proceed" Then I should see "You are too young to create an account on this site." And I should see "Please ask your parent/guardian to contact:" # Try to access the sign up page again. When I click on "Back to the site home" "link" And I click on "Log in" "link" in the ".logininfo" "css_element" And I click on "Create new account" "link" Then I should see "You are too young to create an account on this site." And I should see "Please ask your parent/guardian to contact:" behat/displayloginfailures.feature000064400000005641152265517610013455 0ustar00@core @core_auth Feature: Test the 'showlogfailures' feature works. In order to see my recent login failures when logging in As a user I need to have at least one failed login attempt and then log in Background: Given the following "users" exist: | username | | teacher1 | And the following config values are set as admin: | displayloginfailures | 1 | # Given the user has at least one failed login attempt, when they login, then they should see both header and footer notices. Scenario: Check that 'displayloginfailures' works without javascript for teachers. # Simulate a log in failure for the teacher. Given I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" And I set the field "Username" to "teacher1" And I set the field "Password" to "wrongpass" And I press "Log in" And I should see "Invalid login, please try again" # Now, log in with the correct credentials. When I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" And I press "Log in" # Confirm the notices are displayed. Then I should see "1 failed logins since your last login" in the ".navbar" "css_element" And I should see "1 failed logins since your last login" in the "page-footer" "region" # Confirm the notices disappear when navigating to another page. And I am on homepage And I should not see "1 failed logins since your last login" in the ".navbar" "css_element" And I should not see "1 failed logins since your last login" in the "page-footer" "region" # Given the user has at least one failed login attempt, when they login, then they should see both header and footer notices. Scenario: Check that 'displayloginfailures' works without javascript for admins. # Simulate a log in failure for the teacher. Given I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" And I set the field "Username" to "admin" And I set the field "Password" to "wrongpass" And I press "Log in" And I should see "Invalid login, please try again" # Now, log in with the correct credentials. When I set the field "Username" to "admin" And I set the field "Password" to "admin" And I press "Log in" # Confirm the notices are displayed. Then I should see "1 failed logins since your last login" in the ".navbar" "css_element" And I should see "1 failed logins since your last login (Logs)" in the "page-footer" "region" # Confirm that the link works and that the notices disappear when navigating to another page. And I click on "Logs" "link" in the "page-footer" "region" And I should see "User login failed" in the "table.reportlog" "css_element" And I should not see "1 failed logins since your last login" in the ".navbar" "css_element" And I should not see "1 failed logins since your last login (Logs)" in the "page-footer" "region" behat/validateagedigitalconsentmap.feature000064400000005025152265517610015114 0ustar00@core @verify_age_location Feature: Test validation of 'Age of digital consent' setting. In order to set the 'Age of digital consent' setting As an admin I need to provide valid data and valid format Background: Given I log in as "admin" And I navigate to "Users > Privacy and policies > Privacy settings" in site administration Scenario: Admin provides valid value for 'Age of digital consent'. Given I set the field "s__agedigitalconsentmap" to multiline: """ *, 16 AT, 14 BE, 14 """ When I press "Save changes" Then I should see "Changes saved" And I should not see "Some settings were not changed due to an error." And I should not see "The digital age of consent is not valid:" Scenario: Admin provides invalid format for 'Age of digital consent'. # Try to set a value with missing space separator Given I set the field "s__agedigitalconsentmap" to multiline: """ *16 AT, 14 BE, 14 """ When I press "Save changes" Then I should not see "Changes saved" And I should see "Some settings were not changed due to an error." And I should see "The digital age of consent is not valid: \"*16\" has more or less than one comma separator." # Try to set a value with missing default age of consent When I set the field "s__agedigitalconsentmap" to multiline: """ AT, 14 BE, 14 """ And I press "Save changes" Then I should not see "Changes saved" And I should see "Some settings were not changed due to an error." And I should see "The digital age of consent is not valid: Default (*) value is missing." Scenario: Admin provides invalid age of consent or country for 'Age of digital consent'. # Try to set a value containing invalid age of consent Given I set the field "s__agedigitalconsentmap" to multiline: """ *, 16 AT, age BE, 14 """ When I press "Save changes" Then I should not see "Changes saved" And I should see "Some settings were not changed due to an error." And I should see "The digital age of consent is not valid: \"age\" is not a valid value for age." # Try to set a value containing invalid country When I set the field "s__agedigitalconsentmap" to multiline: """ *, 16 COUNTRY, 14 BE, 14 """ And I press "Save changes" Then I should not see "Changes saved" And I should see "Some settings were not changed due to an error." And I should see "The digital age of consent is not valid: \"COUNTRY\" is not a valid value for country." digital_consent_test.php000064400000014654152265517610011506 0ustar00. namespace core_auth; /** * Digital consent helper testcase. * * @package core_auth * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class digital_consent_test extends \advanced_testcase { public function test_is_age_digital_consent_verification_enabled(): void { global $CFG; $this->resetAfterTest(); // Age of digital consent verification is enabled. $CFG->agedigitalconsentverification = 0; $isenabled = \core_auth\digital_consent::is_age_digital_consent_verification_enabled(); $this->assertFalse($isenabled); } public function test_is_minor(): void { global $CFG; $this->resetAfterTest(); $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT, 14', 'CZ, 13', 'DE, 14', 'DK, 13', ]); $CFG->agedigitalconsentmap = $agedigitalconsentmap; $usercountry1 = 'DK'; $usercountry2 = 'AU'; $userage1 = 12; $userage2 = 14; $userage3 = 16; // Test country exists in agedigitalconsentmap and user age is below the particular digital minor age. $isminor = \core_auth\digital_consent::is_minor($userage1, $usercountry1); $this->assertTrue($isminor); // Test country exists in agedigitalconsentmap and user age is above the particular digital minor age. $isminor = \core_auth\digital_consent::is_minor($userage2, $usercountry1); $this->assertFalse($isminor); // Test country does not exists in agedigitalconsentmap and user age is below the particular digital minor age. $isminor = \core_auth\digital_consent::is_minor($userage2, $usercountry2); $this->assertTrue($isminor); // Test country does not exists in agedigitalconsentmap and user age is above the particular digital minor age. $isminor = \core_auth\digital_consent::is_minor($userage3, $usercountry2); $this->assertFalse($isminor); } public function test_parse_age_digital_consent_map_valid_format(): void { // Value of agedigitalconsentmap has a valid format. $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT, 14', 'BE, 13' ]); $ageconsentmapparsed = \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); $this->assertEquals([ '*' => 16, 'AT' => 14, 'BE' => 13 ], $ageconsentmapparsed ); } public function test_parse_age_digital_consent_map_invalid_format_missing_spaces(): void { // Value of agedigitalconsentmap has an invalid format (missing space separator between values). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT14', ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidcomma', 'error', 'AT14')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_missing_default_value(): void { // Value of agedigitalconsentmap has an invalid format (missing default value). $agedigitalconsentmap = implode(PHP_EOL, [ 'BE, 16', 'AT, 14' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvaliddefault', 'error')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_invalid_country(): void { // Value of agedigitalconsentmap has an invalid format (invalid value for country). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'TEST, 14' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidcountry', 'error', 'TEST')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_invalid_age_string(): void { // Value of agedigitalconsentmap has an invalid format (string value for age). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT, ten' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidage', 'error', 'ten')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_missing_age(): void { // Value of agedigitalconsentmap has an invalid format (missing value for age). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT, ' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidage', 'error', '')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_missing_country(): void { // Value of agedigitalconsentmap has an invalid format (missing value for country). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', ', 12' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidcountry', 'error', '')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } } lib_test.php000064400000164004152266350000007066 0ustar00. /** * Unit tests for (some of) mod/assign/lib.php. * * @package mod_assign * @category phpunit * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_assign; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/assign/lib.php'); require_once($CFG->dirroot . '/mod/assign/locallib.php'); require_once($CFG->dirroot . '/mod/assign/tests/generator.php'); use core_calendar\local\api as calendar_local_api; use core_calendar\local\event\container as calendar_event_container; use mod_assign_test_generator; /** * Unit tests for (some of) mod/assign/lib.php. * * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class lib_test extends \advanced_testcase { // Use the generator helper. use mod_assign_test_generator; /** * Test that assign_print_recent_activity shows ungraded submitted assignments. */ public function test_print_recent_activity(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); $this->submit_for_grading($student, $assign); $this->setUser($teacher); $this->expectOutputRegex('/submitted:/'); assign_print_recent_activity($course, true, time() - 3600); } /** * Test that assign_print_recent_activity does not display any warnings when a custom fullname has been configured. */ public function test_print_recent_activity_fullname(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); $this->submit_for_grading($student, $assign); $this->setUser($teacher); $this->expectOutputRegex('/submitted:/'); set_config('fullnamedisplay', 'firstname, lastnamephonetic'); assign_print_recent_activity($course, false, time() - 3600); } /** * Test that assign_print_recent_activity shows the blind marking ID. */ public function test_print_recent_activity_fullname_blind_marking(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, [ 'blindmarking' => 1, ]); $this->add_submission($student, $assign); $this->submit_for_grading($student, $assign); $this->setUser($teacher); $uniqueid = $assign->get_uniqueid_for_user($student->id); $expectedstr = preg_quote(get_string('participant', 'mod_assign'), '/') . '.*' . $uniqueid; $this->expectOutputRegex("/{$expectedstr}/"); assign_print_recent_activity($course, false, time() - 3600); } /** * Test that assign_get_recent_mod_activity fetches the assignment correctly. */ public function test_assign_get_recent_mod_activity(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); $this->add_submission($student, $assign); $this->submit_for_grading($student, $assign); $index = 1; $activities = [ $index => (object) [ 'type' => 'assign', 'cmid' => $assign->get_course_module()->id, ], ]; $this->setUser($teacher); assign_get_recent_mod_activity($activities, $index, time() - HOURSECS, $course->id, $assign->get_course_module()->id); $activity = $activities[1]; $this->assertEquals("assign", $activity->type); $this->assertEquals($student->id, $activity->user->id); } /** * Ensure that assign_user_complete displays information about drafts. */ public function test_assign_user_complete(): void { global $PAGE, $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, ['submissiondrafts' => 1]); $this->add_submission($student, $assign); $PAGE->set_url(new \moodle_url('/mod/assign/view.php', array('id' => $assign->get_course_module()->id))); $submission = $assign->get_user_submission($student->id, true); $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT; $DB->update_record('assign_submission', $submission); $this->expectOutputRegex('/Draft/'); assign_user_complete($course, $student, $assign->get_course_module(), $assign->get_instance()); } /** * Ensure that assign_user_outline fetches updated grades. */ public function test_assign_user_outline(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); $this->add_submission($student, $assign); $this->submit_for_grading($student, $assign); $this->mark_submission($teacher, $assign, $student, 50.0); $this->setUser($teacher); $data = $assign->get_user_grade($student->id, true); $data->grade = '50.5'; $assign->update_grade($data); $result = assign_user_outline($course, $student, $assign->get_course_module(), $assign->get_instance()); $this->assertMatchesRegularExpression('/50.5/', $result->info); } /** * Tests for mod_assign_refresh_events. */ public function test_assign_refresh_events(): void { global $DB; $this->resetAfterTest(); $duedate = time(); $newduedate = $duedate + DAYSECS; $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, [ 'duedate' => $duedate, ]); $instance = $assign->get_instance(); $eventparams = [ 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => ASSIGN_EVENT_TYPE_DUE, 'groupid' => 0 ]; // Make sure the calendar event for assignment 1 matches the initial due date. $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST); $this->assertEquals($eventtime, $duedate); // Manually update assignment 1's due date. $DB->update_record('assign', (object) [ 'id' => $instance->id, 'duedate' => $newduedate, 'course' => $course->id ]); // Then refresh the assignment events of assignment 1's course. $this->assertTrue(assign_refresh_events($course->id)); // Confirm that the assignment 1's due date event now has the new due date after refresh. $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST); $this->assertEquals($eventtime, $newduedate); // Create a second course and assignment. $othercourse = $this->getDataGenerator()->create_course();; $otherassign = $this->create_instance($othercourse, [ 'duedate' => $duedate, ]); $otherinstance = $otherassign->get_instance(); // Manually update assignment 1 and 2's due dates. $newduedate += DAYSECS; $DB->update_record('assign', (object)[ 'id' => $instance->id, 'duedate' => $newduedate, 'course' => $course->id ]); $DB->update_record('assign', (object)[ 'id' => $otherinstance->id, 'duedate' => $newduedate, 'course' => $othercourse->id ]); // Refresh events of all courses and check the calendar events matches the new date. $this->assertTrue(assign_refresh_events()); // Check the due date calendar event for assignment 1. $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST); $this->assertEquals($eventtime, $newduedate); // Check the due date calendar event for assignment 2. $eventparams['instance'] = $otherinstance->id; $eventtime = $DB->get_field('event', 'timestart', $eventparams, MUST_EXIST); $this->assertEquals($eventtime, $newduedate); // In case the course ID is passed as a numeric string. $this->assertTrue(assign_refresh_events('' . $course->id)); // Non-existing course ID. $this->assertFalse(assign_refresh_events(-1)); // Invalid course ID. $this->assertFalse(assign_refresh_events('aaa')); } public function test_assign_core_calendar_is_event_visible_duedate_event_as_teacher(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $assign = $this->create_instance($course); $this->setAdminUser(); // Create a calendar event. $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); // The teacher should see the due date event. $this->setUser($teacher); $this->assertTrue(mod_assign_core_calendar_is_event_visible($event)); } public function test_assign_core_calendar_is_event_visible_duedate_event_for_teacher(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $assign = $this->create_instance($course); $this->setAdminUser(); // Create a calendar event. $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); // Now, log out. $this->setUser(); // The teacher should see the due date event. $this->assertTrue(mod_assign_core_calendar_is_event_visible($event, $teacher->id)); } public function test_assign_core_calendar_is_event_visible_duedate_event_as_student(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]); $this->setAdminUser(); // Create a calendar event. $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); // The student should care about the due date event. $this->setUser($student); $this->assertTrue(mod_assign_core_calendar_is_event_visible($event)); } public function test_assign_core_calendar_is_event_visible_duedate_event_for_student(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]); $this->setAdminUser(); // Create a calendar event. $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); // Now, log out. $this->setUser(); // The student should care about the due date event. $this->assertTrue(mod_assign_core_calendar_is_event_visible($event, $student->id)); } public function test_assign_core_calendar_is_event_visible_gradingduedate_event_as_teacher(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $assign = $this->create_instance($course); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE); // The teacher should see the due date event. $this->setUser($teacher); $this->assertTrue(mod_assign_core_calendar_is_event_visible($event)); } public function test_assign_core_calendar_is_event_visible_gradingduedate_event_for_teacher(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $assign = $this->create_instance($course); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE); // Now, log out. $this->setUser(); // The teacher should see the due date event. $this->assertTrue(mod_assign_core_calendar_is_event_visible($event, $teacher->id)); } public function test_assign_core_calendar_is_event_visible_gradingduedate_event_as_student(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE); // The student should not see the due date event. $this->setUser($student); $this->assertFalse(mod_assign_core_calendar_is_event_visible($event)); } public function test_assign_core_calendar_is_event_visible_gradingduedate_event_for_student(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE); // Now, log out. $this->setUser(); // The student should not see the due date event. $this->assertFalse(mod_assign_core_calendar_is_event_visible($event, $student->id)); } public function test_assign_core_calendar_provide_event_action_duedate_as_teacher(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $assign = $this->create_instance($course); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); // The teacher should see the event. $this->setUser($teacher); $factory = new \core_calendar\action_factory(); $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory); // The teacher should not have an action for a due date event. $this->assertNull($actionevent); } public function test_assign_core_calendar_provide_event_action_duedate_for_teacher(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $assign = $this->create_instance($course); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); // Now, log out. $this->setUser(); // Decorate action event for a teacher. $factory = new \core_calendar\action_factory(); $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory, $teacher->id); // The teacher should not have an action for a due date event. $this->assertNull($actionevent); } public function test_assign_core_calendar_provide_event_action_duedate_as_student(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); // The student should see the event. $this->setUser($student); $factory = new \core_calendar\action_factory(); $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('addsubmission', 'assign'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } public function test_assign_core_calendar_provide_event_action_duedate_for_student(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); // Now, log out. $this->setUser(); // Decorate action event for a student. $factory = new \core_calendar\action_factory(); $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory, $student->id); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('addsubmission', 'assign'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } /** * Test group submissions. * @covers \assign::mod_assign_core_calendar_provide_event_action */ public function test_assign_core_calendar_provide_event_action_duedate_for_group_assignment(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $group = $this->getDataGenerator()->create_group([ 'courseid' => $course->id, ]); groups_add_member($group->id, $student1->id); groups_add_member($group->id, $student2->id); $assign = $this->create_instance($course, [ 'assignsubmission_onlinetext_enabled' => 1, 'requiresubmissionstatement' => 1, 'teamsubmission' => 1, 'completion' => 0, 'completionsubmit' => 1, ]); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); $this->setUser(); $factory = new \core_calendar\action_factory(); $actionevent1 = mod_assign_core_calendar_provide_event_action($event, $factory, $student1->id); $actionevent2 = mod_assign_core_calendar_provide_event_action($event, $factory, $student2->id); // Confirm the events were decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent1); $this->assertEquals(get_string('addsubmission', 'assign'), $actionevent1->get_name()); $this->assertInstanceOf('moodle_url', $actionevent1->get_url()); $this->assertEquals(1, $actionevent1->get_item_count()); $this->assertTrue($actionevent1->is_actionable()); $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent2); $this->assertEquals(get_string('addsubmission', 'assign'), $actionevent2->get_name()); $this->assertInstanceOf('moodle_url', $actionevent2->get_url()); $this->assertEquals(1, $actionevent2->get_item_count()); $this->assertTrue($actionevent2->is_actionable()); // Submit as the student. $this->add_submission($student1, $assign); $this->submit_for_grading($student1, $assign, ['submissionstatement' => 'Hello, world!']); // Create a new calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); $this->setUser(); // Confirm there were no events to action. $actionevent1 = mod_assign_core_calendar_provide_event_action($event, $factory, $student1->id); $this->assertNull($actionevent1); $actionevent2 = mod_assign_core_calendar_provide_event_action($event, $factory, $student2->id); $this->assertNull($actionevent2); } public function test_assign_core_calendar_provide_event_action_gradingduedate_as_teacher(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $assign = $this->create_instance($course); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE); $this->setUser($teacher); $factory = new \core_calendar\action_factory(); $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('gradenoun'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(0, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } public function test_assign_core_calendar_provide_event_action_gradingduedate_for_teacher(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $assign = $this->create_instance($course); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE); // Now, log out. $this->setUser(); // Decorate action event for a teacher. $factory = new \core_calendar\action_factory(); $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory, $teacher->id); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('gradenoun'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(0, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } public function test_assign_core_calendar_provide_event_action_gradingduedate_as_student(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE); $this->setUser($student); $factory = new \core_calendar\action_factory(); $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('gradenoun'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(0, $actionevent->get_item_count()); $this->assertFalse($actionevent->is_actionable()); } public function test_assign_core_calendar_provide_event_action_gradingduedate_for_student(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course); // Create a calendar event. $this->setAdminUser(); $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_GRADINGDUE); // Now, log out. $this->setUser(); // Decorate action event for a student. $factory = new \core_calendar\action_factory(); $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory, $student->id); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('gradenoun'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(0, $actionevent->get_item_count()); $this->assertFalse($actionevent->is_actionable()); } public function test_assign_core_calendar_provide_event_action_duedate_as_student_submitted(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]); $this->setAdminUser(); // Create a calendar event. $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); // Create an action factory. $factory = new \core_calendar\action_factory(); // Submit as the student. $this->add_submission($student, $assign); $this->submit_for_grading($student, $assign); // Confirm there was no event to action. $factory = new \core_calendar\action_factory(); $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory); $this->assertNull($actionevent); } public function test_assign_core_calendar_provide_event_action_duedate_for_student_submitted(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $assign = $this->create_instance($course, ['assignsubmission_onlinetext_enabled' => 1]); $this->setAdminUser(); // Create a calendar event. $event = $this->create_action_event($course, $assign, ASSIGN_EVENT_TYPE_DUE); // Create an action factory. $factory = new \core_calendar\action_factory(); // Submit as the student. $this->add_submission($student, $assign); $this->submit_for_grading($student, $assign); // Now, log out. $this->setUser(); // Confirm there was no event to action. $factory = new \core_calendar\action_factory(); $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory, $student->id); $this->assertNull($actionevent); } public function test_assign_core_calendar_provide_event_action_already_completed(): void { $this->resetAfterTest(); set_config('enablecompletion', 1); $this->setAdminUser(); // Create the activity. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $assign = $this->create_instance($course, ['completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS]); // Get some additional data. $cm = get_coursemodule_from_instance('assign', $assign->get_instance()->id); // Create a calendar event. $event = $this->create_action_event($course, $assign, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Mark the activity as completed. $completion = new \completion_info($course); $completion->set_module_viewed($cm); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory); // Ensure result was null. $this->assertNull($actionevent); } public function test_assign_core_calendar_provide_event_action_already_completed_for_user(): void { $this->resetAfterTest(); set_config('enablecompletion', 1); $this->setAdminUser(); // Create the activity. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $assign = $this->create_instance($course, ['completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS]); // Enrol a student in the course. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Get some additional data. $cm = get_coursemodule_from_instance('assign', $assign->get_instance()->id); // Create a calendar event. $event = $this->create_action_event($course, $assign, \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED); // Mark the activity as completed for the student. $completion = new \completion_info($course); $completion->set_module_viewed($cm, $student->id); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_assign_core_calendar_provide_event_action($event, $factory, $student->id); // Ensure result was null. $this->assertNull($actionevent); } /** * Creates an action event. * * @param \stdClass $course The course the assignment is in * @param assign $assign The assignment to create an event for * @param string $eventtype The event type. eg. ASSIGN_EVENT_TYPE_DUE. * @return bool|calendar_event */ private function create_action_event($course, $assign, $eventtype) { $event = new \stdClass(); $event->name = 'Calendar event'; $event->modulename = 'assign'; $event->courseid = $course->id; $event->instance = $assign->get_instance()->id; $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = $eventtype; $event->timestart = time(); return \calendar_event::create($event); } /** * Test the callback responsible for returning the completion rule descriptions. * This function should work given either an instance of the module (cm_info), such as when checking the active rules, * or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type. */ public function test_mod_assign_completion_get_active_rule_descriptions(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); $this->setAdminUser(); // Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't. $cm1 = $this->create_instance($course, ['completion' => '2', 'completionsubmit' => '1'])->get_course_module(); $cm2 = $this->create_instance($course, ['completion' => '2', 'completionsubmit' => '0'])->get_course_module(); // Data for the stdClass input type. // This type of input would occur when checking the default completion rules for an activity type, where we don't have // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info. $moddefaults = (object) [ 'customdata' => [ 'customcompletionrules' => [ 'completionsubmit' => '1', ], ], 'completion' => 2, ]; $activeruledescriptions = [get_string('completionsubmit', 'assign')]; $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($cm1), $activeruledescriptions); $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($cm2), []); $this->assertEquals(mod_assign_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions); $this->assertEquals(mod_assign_get_completion_active_rule_descriptions(new \stdClass()), []); } /** * Test that if some grades are not set, they are left alone and not rescaled */ public function test_assign_rescale_activity_grades_some_unset(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $otherstudent = $this->getDataGenerator()->create_and_enrol($course, 'student'); // As a teacher. $this->setUser($teacher); $assign = $this->create_instance($course); // Grade the student. $data = ['grade' => 50]; $assign->testable_apply_grade_to_user((object)$data, $student->id, 0); // Try getting another students grade. This will give a grade of ASSIGN_GRADE_NOT_SET (-1). $assign->get_user_grade($otherstudent->id, true); // Rescale. assign_rescale_activity_grades($course, $assign->get_course_module(), 0, 100, 0, 10); // Get the grades for both students. $studentgrade = $assign->get_user_grade($student->id, true); $otherstudentgrade = $assign->get_user_grade($otherstudent->id, true); // Make sure the real grade is scaled, but the ASSIGN_GRADE_NOT_SET stays the same. $this->assertEquals($studentgrade->grade, 5); $this->assertEquals($otherstudentgrade->grade, ASSIGN_GRADE_NOT_SET); } /** * Return false when there are not overrides for this assign instance. */ public function test_assign_is_override_calendar_event_no_override(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setAdminUser(); $duedate = time(); $assign = $this->create_instance($course, ['duedate' => $duedate]); $instance = $assign->get_instance(); $event = new \calendar_event((object)[ 'modulename' => 'assign', 'instance' => $instance->id, 'userid' => $student->id, ]); $this->assertFalse($assign->is_override_calendar_event($event)); } /** * Return false if the given event isn't an assign module event. */ public function test_assign_is_override_calendar_event_no_nodule_event(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setAdminUser(); $userid = $student->id; $duedate = time(); $assign = $this->create_instance($course, ['duedate' => $duedate]); $instance = $assign->get_instance(); $event = new \calendar_event((object)[ 'userid' => $userid ]); $this->assertFalse($assign->is_override_calendar_event($event)); } /** * Return false if there is overrides for this use but they belong to another assign * instance. */ public function test_assign_is_override_calendar_event_different_assign_instance(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setAdminUser(); $duedate = time(); $assign = $this->create_instance($course, ['duedate' => $duedate]); $instance = $assign->get_instance(); $otherassign = $this->create_instance($course, ['duedate' => $duedate]); $otherinstance = $otherassign->get_instance(); $event = new \calendar_event((object) [ 'modulename' => 'assign', 'instance' => $instance->id, 'userid' => $student->id, ]); $DB->insert_record('assign_overrides', (object) [ 'assignid' => $otherinstance->id, 'userid' => $student->id, ]); $this->assertFalse($assign->is_override_calendar_event($event)); } /** * Return true if there is a user override for this event and assign instance. */ public function test_assign_is_override_calendar_event_user_override(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setAdminUser(); $duedate = time(); $assign = $this->create_instance($course, ['duedate' => $duedate]); $instance = $assign->get_instance(); $event = new \calendar_event((object) [ 'modulename' => 'assign', 'instance' => $instance->id, 'userid' => $student->id, ]); $DB->insert_record('assign_overrides', (object) [ 'assignid' => $instance->id, 'userid' => $student->id, ]); $this->assertTrue($assign->is_override_calendar_event($event)); } /** * Return true if there is a group override for the event and assign instance. */ public function test_assign_is_override_calendar_event_group_override(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $this->setAdminUser(); $duedate = time(); $assign = $this->create_instance($course, ['duedate' => $duedate]); $instance = $assign->get_instance(); $group = $this->getDataGenerator()->create_group(array('courseid' => $instance->course)); $event = new \calendar_event((object) [ 'modulename' => 'assign', 'instance' => $instance->id, 'groupid' => $group->id, ]); $DB->insert_record('assign_overrides', (object) [ 'assignid' => $instance->id, 'groupid' => $group->id, ]); $this->assertTrue($assign->is_override_calendar_event($event)); } /** * Unknown event types should not have any limit restrictions returned. */ public function test_mod_assign_core_calendar_get_valid_event_timestart_range_unkown_event_type(): void { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $this->setAdminUser(); $duedate = time(); $assign = $this->create_instance($course, ['duedate' => $duedate]); $instance = $assign->get_instance(); $event = new \calendar_event((object) [ 'courseid' => $instance->course, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => 'SOME RANDOM EVENT' ]); list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance); $this->assertNull($min); $this->assertNull($max); } /** * Override events should not have any limit restrictions returned. */ public function test_mod_assign_core_calendar_get_valid_event_timestart_range_override_event(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setAdminUser(); $duedate = time(); $assign = $this->create_instance($course, ['duedate' => $duedate]); $instance = $assign->get_instance(); $event = new \calendar_event((object) [ 'courseid' => $instance->course, 'modulename' => 'assign', 'instance' => $instance->id, 'userid' => $student->id, 'eventtype' => ASSIGN_EVENT_TYPE_DUE ]); $record = (object) [ 'assignid' => $instance->id, 'userid' => $student->id, ]; $DB->insert_record('assign_overrides', $record); list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance); $this->assertFalse($min); $this->assertFalse($max); } /** * Assignments configured without a submissions from and cutoff date should not have * any limits applied. */ public function test_mod_assign_core_calendar_get_valid_event_timestart_range_due_no_limit(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $this->setAdminUser(); $duedate = time(); $assign = $this->create_instance($course, [ 'duedate' => $duedate, 'allowsubmissionsfromdate' => 0, 'cutoffdate' => 0, ]); $instance = $assign->get_instance(); $event = new \calendar_event((object) [ 'courseid' => $instance->course, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => ASSIGN_EVENT_TYPE_DUE ]); list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance); $this->assertNull($min); $this->assertNull($max); } /** * Assignments should be bottom and top bound by the submissions from date and cutoff date * respectively. */ public function test_mod_assign_core_calendar_get_valid_event_timestart_range_due_with_limits(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $this->setAdminUser(); $duedate = time(); $submissionsfromdate = $duedate - DAYSECS; $cutoffdate = $duedate + DAYSECS; $assign = $this->create_instance($course, [ 'duedate' => $duedate, 'allowsubmissionsfromdate' => $submissionsfromdate, 'cutoffdate' => $cutoffdate, ]); $instance = $assign->get_instance(); $event = new \calendar_event((object) [ 'courseid' => $instance->course, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => ASSIGN_EVENT_TYPE_DUE ]); list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance); $this->assertEquals($submissionsfromdate, $min[0]); $this->assertNotEmpty($min[1]); $this->assertEquals($cutoffdate, $max[0]); $this->assertNotEmpty($max[1]); } /** * Assignment grading due date should not have any limits of no due date and cutoff date is set. */ public function test_mod_assign_core_calendar_get_valid_event_timestart_range_gradingdue_no_limit(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $this->setAdminUser(); $assign = $this->create_instance($course, [ 'duedate' => 0, 'allowsubmissionsfromdate' => 0, 'cutoffdate' => 0, ]); $instance = $assign->get_instance(); $event = new \calendar_event((object) [ 'courseid' => $instance->course, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE ]); list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance); $this->assertNull($min); $this->assertNull($max); } /** * Assignment grading due event is minimum bound by the due date, if it is set. */ public function test_mod_assign_core_calendar_get_valid_event_timestart_range_gradingdue_with_due_date(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $this->setAdminUser(); $duedate = time(); $assign = $this->create_instance($course, ['duedate' => $duedate]); $instance = $assign->get_instance(); $event = new \calendar_event((object) [ 'courseid' => $instance->course, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE ]); list($min, $max) = mod_assign_core_calendar_get_valid_event_timestart_range($event, $instance); $this->assertEquals($duedate, $min[0]); $this->assertNotEmpty($min[1]); $this->assertNull($max); } /** * Non due date events should not update the assignment due date. */ public function test_mod_assign_core_calendar_event_timestart_updated_non_due_event(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setAdminUser(); $duedate = time(); $submissionsfromdate = $duedate - DAYSECS; $cutoffdate = $duedate + DAYSECS; $assign = $this->create_instance($course, [ 'duedate' => $duedate, 'allowsubmissionsfromdate' => $submissionsfromdate, 'cutoffdate' => $cutoffdate, ]); $instance = $assign->get_instance(); $event = new \calendar_event((object) [ 'courseid' => $instance->course, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => ASSIGN_EVENT_TYPE_GRADINGDUE, 'timestart' => $duedate + 1 ]); mod_assign_core_calendar_event_timestart_updated($event, $instance); $newinstance = $DB->get_record('assign', ['id' => $instance->id]); $this->assertEquals($duedate, $newinstance->duedate); } /** * Due date override events should not change the assignment due date. */ public function test_mod_assign_core_calendar_event_timestart_updated_due_event_override(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setAdminUser(); $duedate = time(); $submissionsfromdate = $duedate - DAYSECS; $cutoffdate = $duedate + DAYSECS; $assign = $this->create_instance($course, [ 'duedate' => $duedate, 'allowsubmissionsfromdate' => $submissionsfromdate, 'cutoffdate' => $cutoffdate, ]); $instance = $assign->get_instance(); $event = new \calendar_event((object) [ 'courseid' => $instance->course, 'modulename' => 'assign', 'instance' => $instance->id, 'userid' => $student->id, 'eventtype' => ASSIGN_EVENT_TYPE_DUE, 'timestart' => $duedate + 1 ]); $record = (object) [ 'assignid' => $instance->id, 'userid' => $student->id, 'duedate' => $duedate + 1, ]; $DB->insert_record('assign_overrides', $record); mod_assign_core_calendar_event_timestart_updated($event, $instance); $newinstance = $DB->get_record('assign', ['id' => $instance->id]); $this->assertEquals($duedate, $newinstance->duedate); } /** * Due date events should update the assignment due date. */ public function test_mod_assign_core_calendar_event_timestart_updated_due_event(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setAdminUser(); $duedate = time(); $newduedate = $duedate + 1; $submissionsfromdate = $duedate - DAYSECS; $cutoffdate = $duedate + DAYSECS; $assign = $this->create_instance($course, [ 'duedate' => $duedate, 'allowsubmissionsfromdate' => $submissionsfromdate, 'cutoffdate' => $cutoffdate, ]); $instance = $assign->get_instance(); $event = new \calendar_event((object) [ 'courseid' => $instance->course, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => ASSIGN_EVENT_TYPE_DUE, 'timestart' => $newduedate ]); mod_assign_core_calendar_event_timestart_updated($event, $instance); $newinstance = $DB->get_record('assign', ['id' => $instance->id]); $this->assertEquals($newduedate, $newinstance->duedate); } /** * If a student somehow finds a way to update the due date calendar event * then the callback should not be executed to update the assignment due * date as well otherwise that would be a security issue. */ public function test_student_role_cant_update_due_event(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $roleid = $this->getDataGenerator()->create_role(); $role = $DB->get_record('role', ['id' => $roleid]); $user = $this->getDataGenerator()->create_and_enrol($course, $role->shortname); $this->setAdminUser(); $mapper = calendar_event_container::get_event_mapper(); $now = time(); $duedate = (new \DateTime())->setTimestamp($now); $newduedate = (new \DateTime())->setTimestamp($now)->modify('+1 day'); $assign = $this->create_instance($course, [ 'course' => $course->id, 'duedate' => $duedate->getTimestamp(), ]); $instance = $assign->get_instance(); $record = $DB->get_record('event', [ 'courseid' => $course->id, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => ASSIGN_EVENT_TYPE_DUE ]); $event = new \calendar_event($record); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleid, $context, true); $this->setUser($user); calendar_local_api::update_event_start_day( $mapper->from_legacy_event_to_event($event), $newduedate ); $newinstance = $DB->get_record('assign', ['id' => $instance->id]); $newevent = \calendar_event::load($event->id); // The due date shouldn't have changed even though we updated the calendar // event. $this->assertEquals($duedate->getTimestamp(), $newinstance->duedate); $this->assertEquals($newduedate->getTimestamp(), $newevent->timestart); } /** * A teacher with the capability to modify an assignment module should be * able to update the assignment due date by changing the due date calendar * event. */ public function test_teacher_role_can_update_due_event(): void { global $CFG, $DB; require_once($CFG->dirroot . '/calendar/lib.php'); $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $user = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $roleid = $DB->get_field('role', 'id', ['shortname' => 'teacher']); $this->setAdminUser(); $mapper = calendar_event_container::get_event_mapper(); $now = time(); $duedate = (new \DateTime())->setTimestamp($now); $newduedate = (new \DateTime())->setTimestamp($now)->modify('+1 day'); $assign = $this->create_instance($course, [ 'course' => $course->id, 'duedate' => $duedate->getTimestamp(), ]); $instance = $assign->get_instance(); $record = $DB->get_record('event', [ 'courseid' => $course->id, 'modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => ASSIGN_EVENT_TYPE_DUE ]); $event = new \calendar_event($record); assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true); assign_capability('moodle/course:manageactivities', CAP_ALLOW, $roleid, $context, true); $this->setUser($user); // Trigger and capture the event when adding a contact. $sink = $this->redirectEvents(); calendar_local_api::update_event_start_day( $mapper->from_legacy_event_to_event($event), $newduedate ); $triggeredevents = $sink->get_events(); $moduleupdatedevents = array_filter($triggeredevents, function($e) { return is_a($e, 'core\event\course_module_updated'); }); $newinstance = $DB->get_record('assign', ['id' => $instance->id]); $newevent = \calendar_event::load($event->id); // The due date shouldn't have changed even though we updated the calendar // event. $this->assertEquals($newduedate->getTimestamp(), $newinstance->duedate); $this->assertEquals($newduedate->getTimestamp(), $newevent->timestart); // Confirm that a module updated event is fired when the module // is changed. $this->assertNotEmpty($moduleupdatedevents); } /** * A user who does not have capabilities to add events to the calendar should be able to create an assignment. */ public function test_creation_with_no_calendar_capabilities(): void { $this->resetAfterTest(); $course = self::getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher'); $roleid = self::getDataGenerator()->create_role(); self::getDataGenerator()->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true); $generator = self::getDataGenerator()->get_plugin_generator('mod_assign'); // Create an instance as a user without the calendar capabilities. $this->setUser($user); $time = time(); $params = array( 'course' => $course->id, 'allowsubmissionsfromdate' => $time, 'duedate' => $time + 500, 'cutoffdate' => $time + 600, 'gradingduedate' => $time + 700, ); $generator->create_instance($params); } } text_filter_test.php000064400000032365152266350000010655 0ustar00. namespace filter_glossary; /** * Unit tests. * * @package filter_glossary * @category test * @copyright 2013 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \filter_glossary\text_filter */ final class text_filter_test extends \advanced_testcase { public function test_link_to_entry_with_alias(): void { global $CFG; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $normal = $generator->create_content( $glossary, ['concept' => 'entry name'], ['first alias', 'second alias'] ); // Format text with all three entries in HTML. $html = '

First we have entry name, then we have it twp aliases first alias and second alias.

'; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 3 glossary links. $this->assertEquals(3, count($matches[1])); $this->assertEquals($normal->id, $matches[1][0]); $this->assertEquals($normal->id, $matches[1][1]); $this->assertEquals($normal->id, $matches[1][2]); // Check text of title attribute. $this->assertEquals($glossary->name . ': entry name', $matches[2][0]); $this->assertEquals($glossary->name . ': first alias', $matches[2][1]); $this->assertEquals($glossary->name . ': second alias', $matches[2][2]); } public function test_longest_link_used(): void { global $CFG; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $shorter = $generator->create_content($glossary, ['concept' => 'Tim']); $longer = $generator->create_content($glossary, ['concept' => 'Time']); // Format text with all three entries in HTML. $html = '

Time will tell

'; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 1 glossary link to Time, not Tim. $this->assertEquals(1, count($matches[1])); $this->assertEquals($longer->id, $matches[1][0]); // Check text of title attribute. $this->assertEquals($glossary->name . ': Time', $matches[2][0]); } public function test_link_to_category(): void { global $CFG; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. /** @var \mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $category = $generator->create_category($glossary, ['name' => 'My category', 'usedynalink' => 1]); // Format text with all three entries in HTML. $html = '

This is My category you know.

'; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. $matches = []; preg_match_all('~hook=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 1 glossary link. $this->assertEquals(1, count($matches[1])); $this->assertEquals($category->id, $matches[1][0]); $this->assertEquals($glossary->name . ': Category My category', $matches[2][0]); } /** * Test ampersands. */ public function test_ampersands(): void { global $CFG; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. /** @var \mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $normal = $generator->create_content($glossary, ['concept' => 'normal']); $amp1 = $generator->create_content($glossary, ['concept' => 'A&B']); $amp2 = $generator->create_content($glossary, ['concept' => 'C&D']); // Format text with all three entries in HTML. $html = '

A&B C&D normal

'; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 3 glossary links. $this->assertEquals(3, count($matches[1])); $this->assertEquals($amp1->id, $matches[1][0]); $this->assertEquals($amp2->id, $matches[1][1]); $this->assertEquals($normal->id, $matches[1][2]); // Check text and escaping of title attribute. $this->assertEquals($glossary->name . ': A&B', $matches[2][0]); $this->assertEquals($glossary->name . ': C&D', $matches[2][1]); $this->assertEquals($glossary->name . ': normal', $matches[2][2]); } /** * Test brackets. */ public function test_brackets(): void { global $CFG; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. /** @var \mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $simple = $generator->create_content($glossary, ['concept' => 'simple']); $withbrackets = $generator->create_content($glossary, ['concept' => 'more complex (perhaps)']); $test2 = $generator->create_content($glossary, ['concept' => 'Test (2)']); // Format text with all three entries in HTML. $html = '

Some thigns are simple. Others are more complex (perhaps). Test (2).

'; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); // Find all the glossary links in the result. $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 3 glossary links. $this->assertEquals(3, count($matches[1])); $this->assertEquals($simple->id, $matches[1][0]); $this->assertEquals($withbrackets->id, $matches[1][1]); $this->assertEquals($test2->id, $matches[1][2]); // Check text and escaping of title attribute. $this->assertEquals($glossary->name . ': simple', $matches[2][0]); $this->assertEquals($glossary->name . ': more complex (perhaps)', $matches[2][1]); $this->assertEquals($glossary->name . ': Test (2)', $matches[2][2]); } public function test_exclude_excludes_link_to_entry_with_alias(): void { global $CFG, $GLOSSARY_EXCLUDEENTRY; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $tobeexcluded = $generator->create_content( $glossary, ['concept' => 'entry name'], ['first alias', 'second alias'] ); $normal = $generator->create_content($glossary, ['concept' => 'other entry']); // Format text with all three entries in HTML. $html = '

First we have entry name, then we have it twp aliases first alias and second alias. ' . 'In this case, those should not be linked, but this other entry should be.

'; $GLOSSARY_EXCLUDEENTRY = $tobeexcluded->id; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); $GLOSSARY_EXCLUDEENTRY = null; // Find all the glossary links in the result. $matches = []; preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 1 glossary links. $this->assertEquals(1, count($matches[1])); $this->assertEquals($normal->id, $matches[1][0]); $this->assertEquals($glossary->name . ': other entry', $matches[2][0]); } public function test_exclude_does_not_exclude_categories(): void { global $CFG, $GLOSSARY_EXCLUDEENTRY; $this->resetAfterTest(true); // Enable glossary filter at top level. filter_set_global_state('glossary', TEXTFILTER_ON); $CFG->glossary_linkentries = 1; // Create a test course. $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Create a glossary. $glossary = $this->getDataGenerator()->create_module( 'glossary', ['course' => $course->id, 'mainglossary' => 1] ); // Create two entries with ampersands and one normal entry. /** @var \mod_glossary_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary'); $category = $generator->create_category($glossary, ['name' => 'My category', 'usedynalink' => 1]); // Format text with all three entries in HTML. $html = '

This is My category you know.

'; $GLOSSARY_EXCLUDEENTRY = $category->id; $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); $GLOSSARY_EXCLUDEENTRY = null; // Find all the glossary links in the result. $matches = []; preg_match_all('~hook=([0-9]+).*?title="(.*?)"~', $filtered, $matches); // There should be 1 glossary link. $this->assertEquals(1, count($matches[1])); $this->assertEquals($category->id, $matches[1][0]); $this->assertEquals($glossary->name . ': Category My category', $matches[2][0]); } } upgradelib_test.php000064400000006071152266351260010446 0ustar00. namespace filter_displayh5p; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/filter/displayh5p/db/upgradelib.php'); require_once("$CFG->libdir/filterlib.php"); /** * Unit tests for the upgradelib of the Display H5P filter. * * @package filter_displayh5p * @category test * @copyright 2019 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class upgradelib_test extends \advanced_testcase { /** * test_filter_displayh5p_reorder */ public function test_filter_displayh5p_reorder(): void { $this->resetAfterTest(true); $this->setAdminUser(); // We disable the three filters involved to reorder. // To do this, we will enable them in order and so they will be placed sorted. filter_set_global_state('displayh5p', TEXTFILTER_DISABLED); filter_set_global_state('activitynames', TEXTFILTER_DISABLED); filter_set_global_state('urltolink', TEXTFILTER_DISABLED); // First, we enabled activitynames and urltolink. // So, displayh5p will be below them. filter_set_global_state('activitynames', TEXTFILTER_ON); filter_set_global_state('urltolink', TEXTFILTER_ON); // We get the new order of the filter. $states = filter_get_global_states(); $displayh5ppos = $states['displayh5p']->sortorder; $activitynamespos = $states['activitynames']->sortorder; $urltolinkpos = $states['urltolink']->sortorder; // Make sure that activitynames and urltolink are over the displayh5p. $this->assertLessThan($displayh5ppos, $activitynamespos); $this->assertLessThan($displayh5ppos, $urltolinkpos); // Call the function to reorder displayh5p. filter_displayh5p_reorder(); // Get the new orders. $states = filter_get_global_states(); $displayh5ppos = $states['displayh5p']->sortorder; $activitynamespos = $states['activitynames']->sortorder; $urltolinkpos = $states['urltolink']->sortorder; // Make sure that displayh5p are over activitynames and urltolink. $this->assertLessThan($activitynamespos, $displayh5ppos); $this->assertLessThan($urltolinkpos, $displayh5ppos); // Make sure that displayh5p is enabled. $this->assertEquals(TEXTFILTER_ON, $states['displayh5p']->active); } } behat/h5p_filter.feature000064400000022652152266351260011264 0ustar00@editor @filter @filter_displayh5p @core_h5p @_file_upload @_switch_iframe Feature: Render H5P content using filters To write rich text - I need to render H5P content. Background: Given the following "courses" exist: | shortname | fullname | | C1 | Course 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the "displayh5p" filter is "on" And the following config values are set as admin: | allowedsources | https://moodle.h5p.com/content/[id]/embed | filter_displayh5p | @javascript @external Scenario: Render an external H5P content URL. Given the following "activities" exist: | activity | name | intro | introformat | course | content | contentformat | idnumber | | page | PageName1 | PageDesc1 | 1 | C1 |
Go for it
https://moodle.h5p.com/content/1290772960722742119/embed | 1 | 1 | When I am on the PageName1 "page activity" page logged in as teacher1 And I switch to "h5p-iframe" class iframe Then I should see "Lorum ipsum" @javascript Scenario: Add an external H5P content URL in a link with the URL. Should be rendered. Given the following "activities" exist: | activity | name | intro | introformat | course | content | contentformat | idnumber | | page | PageName1 | PageDesc1 | 1 | C1 | https://moodle.h5p.com/content/1290772960722742119/embed | 1 | 1 | When I am on the PageName1 "page activity" page logged in as teacher1 And I switch to "h5p-iframe" class iframe Then I should see "Lorum ipsum" Scenario: Add an external H5P content URL in a link with text. Shouldn't be rendered. # This content won't be displayed, so this scenario shouldn't be labeled as external. Given the following "activities" exist: | activity | name | intro | introformat | course | content | contentformat | idnumber | | page | PageName1 | PageDesc1 | 1 | C1 | Here you are the content | 1 | 1 | When I am on the PageName1 "page activity" page logged in as teacher1 Then ".h5p-iframe" "css_element" should not exist @javascript Scenario: Render a local H5P file as admin Given the following "activities" exist: | activity | name | intro | introformat | course | content | contentformat | idnumber | [[files::content]] | | page | PageName1 | PageDesc1 | 1 | C1 |

H5P Activity:

@@PLUGINFILE@@/ipsums.h5p
| 1 | 1 | h5p/tests/fixtures/ipsums.h5p::ipsums.h5p | When I am on the PageName1 "page activity" page logged in as teacher1 And I should see "PageName1" in the "page-header" "region" # Switch to iframe created by filter And I switch to "h5p-iframe" class iframe # Switch to iframe created by embed.php page And I switch to "h5p-iframe" class iframe Then I should see "Lorum ipsum" And I switch to the main frame And I log out And I am on the PageName1 "page activity" page logged in as student1 # Switch to iframe created by filter And I switch to "h5p-iframe" class iframe # Switch to iframe created by embed.php page And I switch to "h5p-iframe" class iframe Then I should not see "you don't have access" And I should see "Lorum ipsum" @javascript Scenario: Render a local H5P file as teacher Given the following "activities" exist: | activity | name | intro | introformat | course | content | contentformat | idnumber | [[files::content]] | | page | PageName1 | PageDesc1 | 1 | C1 |

H5P Activity:

@@PLUGINFILE@@/ipsums.h5p
| 1 | 1 | h5p/tests/fixtures/ipsums.h5p::ipsums.h5p::teacher1 | When I am on the PageName1 "page activity" page logged in as teacher1 And I should see "PageName1" in the "page-header" "region" # Switch to iframe created by filter And I switch to "h5p-iframe" class iframe Then I should see "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries." And I should see "missing-required-library" @javascript Scenario: Render a local H5P file with existing libraries Given the following "activities" exist: | activity | name | intro | introformat | course | content | contentformat | idnumber | [[files::content]] | | page | PageName1 | PageDesc1 | 1 | C1 |

H5P Activity:

@@PLUGINFILE@@/ipsums.h5p
| 1 | 1 | h5p/tests/fixtures/ipsums.h5p::ipsums.h5p::teacher1 | | page | PageName2 | PageDesc2 | 1 | C1 |

H5P Activity:

@@PLUGINFILE@@/ipsums.h5p
| 1 | 1 | h5p/tests/fixtures/ipsums.h5p::ipsums.h5p::admin | When I am on the PageName1 "page activity" page logged in as teacher1 And I should see "PageName1" in the "page-header" "region" # Switch to iframe created by filter And I switch to "h5p-iframe" class iframe # Libraries don't exist, so an error should be displayed. Then I should see "missing-required-library" And I switch to the main frame And I am on the PageName2 "page activity" page logged in as admin And I should see "PageName2" in the "page-header" "region" # Switch to iframe created by filter And I switch to "h5p-iframe" class iframe # Switch to iframe created by embed.php page And I switch to "h5p-iframe" class iframe # Libraries have been installed. Then I should see "Lorum ipsum" And I switch to the main frame And I am on the PageName1 "page activity" page logged in as teacher1 # Switch to iframe created by filter And I switch to "h5p-iframe" class iframe # Switch to iframe created by embed.php page And I switch to "h5p-iframe" class iframe Then I should not see "missing-required-library" And I should see "Lorum ipsum" @javascript Scenario: Render local H5P file with a disabled main library Given the following "activities" exist: | activity | name | intro | introformat | course | content | contentformat | idnumber | [[files::content]] | | page | PageName1 | PageDesc1 | 1 | C1 |

H5P Activity:

@@PLUGINFILE@@/ipsums.h5p
| 1 | 1 | h5p/tests/fixtures/ipsums.h5p::ipsums.h5p | When I am logged in as "admin" # Upload manually the H5P content-type library and disable it. And I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And I click on "Disable" "link" in the "Accordion" "table_row" And I am on the PageName1 "page activity" page logged in as admin And I should see "PageName1" in the "page-header" "region" And I switch to "h5p-iframe" class iframe # Library is disabled, so an error should be displayed. Then I should see "This file can't be displayed because its content type is disabled." And I should not see "Lorum ipsum" And I switch to the main frame And I navigate to "H5P > Manage H5P content types" in site administration And I click on "Enable" "link" in the "Accordion" "table_row" # Content should be deployed now that main library is enabled. And I am on the PageName1 "page activity" page # Switch to iframe created by filter. And I switch to "h5p-iframe" class iframe # Switch to iframe created by embed.php page. And I switch to "h5p-iframe" class iframe Then I should see "Lorum ipsum" And I should not see "This file can't be displayed because its content type is disabled." And I switch to the main frame And I navigate to "H5P > Manage H5P content types" in site administration And I click on "Disable" "link" in the "Accordion" "table_row" # Library is disabled again, so an error should be displayed. And I am on the PageName1 "page activity" page And I switch to "h5p-iframe" class iframe Then I should see "This file can't be displayed because its content type is disabled." And I should not see "Lorum ipsum" And I switch to the main frame behat/inline_editing_content.feature000064400000030616152266351260013735 0ustar00@editor @filter @filter_displayh5p @core_h5p @_switch_iframe Feature: Inline editing H5P content anywhere In order to edit an existing H5P content As a user I need to see the button and access to the H5P editor Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | teacher2 | Teacher | 2 | teacher2@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | teacher | | student1 | C1 | student | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | Greeting card | /h5p/tests/fixtures/greeting-card.h5p | And the following "activities" exist: | activity | name | intro | introformat | course | content | contentformat | idnumber | | page | PageName1 | PageDesc1 | 1 | C1 | H5Ptest | 1 | 1 | And the "displayh5p" filter is "on" # Override this capability to let teachers and students to Turn editing on. And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/course:update | Allow | teacher | System | | | moodle/course:update | Allow | student | System | | @javascript @mod @mod_page Scenario: Edit H5P content from a page using link to private file Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/h5p:updatelibraries | Allow | editingteacher | System | | And the following "user private file" exists: | user | teacher1 | | filepath | h5p/tests/fixtures/greeting-card.h5p | # Add H5P content to the page. And I am on the "PageName1" "page activity" page logged in as "teacher1" And I navigate to "Settings" in current page administration And I click on "Insert H5P content" "button" in the "#fitem_id_page" "css_element" And I click on "Browse repositories..." "button" in the "Insert H5P content" "dialogue" And I select "Private files" repository in file picker And I click on "greeting-card.h5p" "file" in repository content area And I click on "Link to the file" "radio" And I click on "Select this file" "button" And I click on "Insert H5P" "button" in the "Insert H5P content" "dialogue" And I click on "Save and display" "button" And I switch to "h5p-iframe" class iframe And I switch to "h5p-iframe" class iframe And I should see "Hello world!" And I switch to the main frame # The Edit button is only displayed when editing mode is on. And I should not see "Edit H5P content" And I am on "Course 1" course homepage with editing mode on And I am on the "PageName1" "page activity" page Then I should see "Edit H5P content" And I log out # Check admin can't see the Edit button (it's a private file and only the author can edit it). And I log in as "admin" And I am on "Course 1" course homepage with editing mode on And I am on the "PageName1" "page activity" page And I should not see "Edit H5P content" And I log out # Check teacher2 (non-editing teacher) can't see the Edit button. And I log in as "teacher2" And I turn editing mode on And I am on "Course 1" course homepage And I am on the "PageName1" "page activity" page And I should not see "Edit H5P content" And I log out # Check student1 can't see the Edit button. And I log in as "student1" And I turn editing mode on And I am on "Course 1" course homepage And I am on the "PageName1" "page activity" page And I should not see "Edit H5P content" @javascript @mod @mod_page @repository_contentbank Scenario: Edit H5P content from a page using link to content bank file Given I am on the "C1" "Course" page logged in as "admin" # Add H5P content to the page. And I am on the "PageName1" "page activity" page And I navigate to "Settings" in current page administration And I click on "Insert H5P content" "button" in the "#fitem_id_page" "css_element" And I click on "Browse repositories..." "button" in the "Insert H5P content" "dialogue" And I select "Content bank" repository in file picker And I click on "Greeting card" "file" in repository content area And I click on "Link to the file" "radio" And I click on "Select this file" "button" And I click on "Insert H5P" "button" in the "Insert H5P content" "dialogue" And I click on "Save and display" "button" And I switch to "h5p-iframe" class iframe And I switch to "h5p-iframe" class iframe And I should see "Hello world!" And I switch to the main frame # The Edit button is only displayed when editing mode is on. And I should not see "Edit H5P content" When I am on "Course 1" course homepage with editing mode on And I am on the "PageName1" "page activity" page Then I should see "Edit H5P content" And I log out # Check teacher1 can see the Edit button too. And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I am on the "PageName1" "page activity" page And I should not see "Edit H5P content" And I log out # Check teacher2 (non-editing teacher) can't see the Edit button. And I log in as "teacher2" And I turn editing mode on And I am on "Course 1" course homepage And I am on the "PageName1" "page activity" page And I should not see "Edit H5P content" And I log out # Check student1 can't see the Edit button. And I log in as "student1" And I turn editing mode on And I am on "Course 1" course homepage And I am on the "PageName1" "page activity" page And I should not see "Edit H5P content" @javascript @mod @mod_page @repository_contentbank Scenario: Edit H5P content from a page using copy to content bank file Given I am on the "C1" "Course" page logged in as "admin" # Add H5P content to the page. And I am on the "PageName1" "page activity" page And I navigate to "Settings" in current page administration And I click on "Insert H5P content" "button" in the "#fitem_id_page" "css_element" And I click on "Browse repositories..." "button" in the "Insert H5P content" "dialogue" And I select "Content bank" repository in file picker And I click on "Greeting card" "file" in repository content area And I click on "Make a copy of the file" "radio" And I click on "Select this file" "button" And I click on "Insert H5P" "button" in the "Insert H5P content" "dialogue" And I click on "Save and display" "button" And I switch to "h5p-iframe" class iframe And I switch to "h5p-iframe" class iframe And I should see "Hello world!" And I switch to the main frame # The Edit button is only displayed when editing mode is on. And I should not see "Edit H5P content" When I am on "Course 1" course homepage with editing mode on And I am on the "PageName1" "page activity" page Then I should see "Edit H5P content" And I log out # Check teacher1 can see the Edit button too. And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I am on the "PageName1" "page activity" page And I should see "Edit H5P content" And I log out # Check teacher2 (non-editing teacher) can't see the Edit button. And I log in as "teacher2" And I turn editing mode on And I am on "Course 1" course homepage And I am on the "PageName1" "page activity" page And I should not see "Edit H5P content" And I log out # Check student1 can't see the Edit button. And I log in as "student1" And I turn editing mode on And I am on "Course 1" course homepage And I am on the "PageName1" "page activity" page And I should not see "Edit H5P content" @javascript @mod @mod_page Scenario: Edit H5P content from a page using external URL Given the following config values are set as admin: | allowedsources | https://moodle.h5p.com/content/[id] | filter_displayh5p | And I am on the "C1" "Course" page logged in as "admin" # Add H5P content to the page. And I am on the "PageName1" "page activity" page And I navigate to "Settings" in current page administration And I click on "Insert H5P content" "button" in the "#fitem_id_page" "css_element" And I set the field "H5P URL or file upload" to "https://moodle.h5p.com/content/1290772960722742119" And I click on "Insert H5P" "button" in the "Insert H5P content" "dialogue" And I click on "Save and display" "button" And ".h5p-placeholder" "css_element" should exist And I switch to "h5pcontent" iframe And I should see "Lorum ipsum" And I switch to the main frame # The Edit button is never displayed (because it's not a local file). And I should not see "Edit H5P content" When I am on "Course 1" course homepage with editing mode on And I am on the "PageName1" "page activity" page Then I should not see "Edit H5P content" And I log out # Check teacher1 can't see the Edit button. And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I am on the "PageName1" "page activity" page And I should not see "Edit H5P content" And I log out # Check teacher2 (non-editing teacher) can't see the Edit button. And I log in as "teacher2" And I turn editing mode on And I am on "Course 1" course homepage And I am on the "PageName1" "page activity" page And I should not see "Edit H5P content" And I log out # Check student1 can't see the Edit button. And I log in as "student1" And I turn editing mode on And I am on "Course 1" course homepage And I am on the "PageName1" "page activity" page And I should not see "Edit H5P content" @javascript @block @block_html @core_block @repository_contentbank Scenario: Edit H5P content from a block using copy to content bank file Given I am on the "C1" "Course" page logged in as "admin" # Add H5P content to the block. And I turn editing mode on And I add the "Text" block to the default region with: | Text block title | H5PTest | | Content | - | And I configure the "H5PTest" block And I click on "Insert H5P content" "button" in the "//div[contains(@id,'fitem_id_config_text')]" "xpath_element" And I click on "Browse repositories..." "button" in the "Insert H5P content" "dialogue" And I select "Content bank" repository in file picker And I click on "Greeting card" "file" in repository content area And I click on "Make a copy of the file" "radio" And I click on "Select this file" "button" And I click on "Insert H5P" "button" in the "Insert H5P content" "dialogue" And I click on "Save changes" "button" in the "Configure H5PTest block" "dialogue" And I switch to "h5p-iframe" class iframe And I switch to "h5p-iframe" class iframe And I should see "Hello world!" And I switch to the main frame # The Edit button is only displayed when editing mode is on. And I should see "Edit H5P content" When I turn editing mode off Then I should not see "Edit H5P content" And I log out # Check teacher1 can see the Edit button too. And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I should see "Edit H5P content" And I log out # Check teacher2 (non-editing teacher) can't see the Edit button. And I log in as "teacher2" And I turn editing mode on And I am on "Course 1" course homepage And I should not see "Edit H5P content" And I log out # Check student1 can't see the Edit button. And I log in as "student1" And I turn editing mode on And I am on "Course 1" course homepage And I should not see "Edit H5P content" behat/course_report_competency.feature000064400000002142152266351310014330 0ustar00@report @report_competency Feature: In a course administration page, navigate through report page, test for course competency page In order to navigate through report page As an admin Go to course administration -> reports -> competency breackdown Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | admin | C1 | editingteacher | | student1 | C1 | student | @javascript Scenario: Selector should be available in the course competency page Given I log in as "admin" And I am on "Course 1" course homepage When I navigate to "Reports" in current page administration And I click on "Competency breakdown" "link" Then "Report" "field" should exist in the "tertiary-navigation" "region" And I should see "Competency breakdown" in the "tertiary-navigation" "region" behat/breakdown_by_activity.feature000064400000007441152266351310013600 0ustar00@report @javascript @report_competency Feature: See the competencies for an activity As a competency grader In order to perform mark all competencies for an activity I need to see the competencies linked to one activity in the breakdown report. Background: Given the following "core_competency > frameworks" exist: | shortname | idnumber | | Test-Framework | ID-FW1 | And the following "core_competency > competencies" exist: | shortname | competencyframework | | Test-Comp1 | ID-FW1 | | Test-Comp2 | ID-FW1 | Given the following "courses" exist: | shortname | fullname | | C1 | Course 1 | And the following "users" exist: | username | firstname | lastname | email | idnumber | middlename | alternatename | firstnamephonetic | lastnamephonetic | | student1 | Grainne | Beauchamp | student1@example.com | s1 | Ann | Jill | Gronya | Beecham | | student2 | Niamh | Cholmondely | student2@example.com | s2 | Jane | Nina | Nee | Chumlee | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | page | PageName1 | PageDesc1 | C1 | PAGE1 | And the following config values are set as admin: | fullnamedisplay | firstname | | alternativefullnameformat | middlename, alternatename, firstname, lastname | And I log in as "admin" And I am on site homepage And I follow "Course 1" And I navigate to "Competencies" in current page administration And I press "Add competencies to course" And "Competency picker" "dialogue" should be visible And I select "Test-Comp1" of the competency tree And I click on "Add" "button" in the "Competency picker" "dialogue" And I press "Add competencies to course" And "Competency picker" "dialogue" should be visible And I select "Test-Comp2" of the competency tree And I click on "Add" "button" in the "Competency picker" "dialogue" And I am on the PageName1 "page activity editing" page And I click on "Expand all" "link" in the "region-main" "region" And I set the field "Course competencies" to "Test-Comp1" And I press "Save and return to course" @javascript Scenario: Go to the competency breakdown report When I navigate to "Reports" in current page administration And I click on "Competency breakdown" "link" And I set the competency filter "Filter competencies by resource or activity" to "PageName1" Then I should see "Test-Comp1" And I should not see "Test-Comp2" And I should see "Ann, Jill, Grainne, Beauchamp" And I should see "Ann, Jill, Grainne, Beauchamp" in the ".form-autocomplete-selection" "css_element" And I open the autocomplete suggestions list And I should see "Jane, Nina, Niamh, Cholmondely" in the ".form-autocomplete-suggestions" "css_element" And I click on "Not rated" "link" And I click on "Rate" "button" And I set the field "Rating" to "A" And I click on "Rate" "button" in the ".competency-grader" "css_element" And I click on "Close" "button" in the "User competency summary" "dialogue" And I click on "PageName1" "autocomplete_selection" And I should see "Test-Comp1" And I should see "Test-Comp2" @accessibility Scenario: Evaluate the accessibility of the user competency summary dialogue Given I navigate to "Reports" in current page administration When I click on "Competency breakdown" "link" And I click on "Not rated" "link" And the page should meet accessibility standards behat/behat_report_competency.php000064400000011162152266351310013251 0ustar00. /** * Behat competency report definitions. * * @package report_competency * @category test * @copyright 2022 Noel De Martin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); /** * Competency report definitions. * * @package report_competency * @category test * @copyright 2022 Noel De Martin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_report_competency extends behat_base { /** * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. * * Recognised page names are: * | pagetype | name meaning | description | * | Breakdown | Course name | The course competencies breakdown page | * * @param string $page identifies which type of page this is, e.g. 'Breakdown'. * @param string $identifier identifies the particular page, e.g. 'C1'. * @return moodle_url the corresponding URL. * @throws Exception with a meaningful error message if the specified page cannot be found. */ protected function resolve_page_instance_url(string $page, string $identifier): moodle_url { switch (strtolower($page)) { case 'breakdown': $courseid = $this->get_course_id($identifier); return new moodle_url('/report/competency/index.php', [ 'id' => $courseid, ]); default: throw new Exception("Unrecognised page type '{$page}'"); } } /** * Return a list of the exact named selectors for the component. * * @return behat_component_named_selector[] */ public static function get_exact_named_selectors(): array { return [ new behat_component_named_selector('breakdown', [ "//*[@data-region='competency-breakdown-report']//table". "//tr[contains(., //a[@data-action='competency-dialogue'][contains(., %locator%)])]", ]), new behat_component_named_selector('breakdown rating', [ "//td[position()=2][contains(., //a[@title='User competency summary'][contains(., %locator%)])]", ]), ]; } /** * Set the value of a competency filter. * * @When /^I set the competency filter "([^"]*)" to "([^"]*)"$/ * @param string $fieldlocator The field locator. * @param string $value The value to set. * @throws Exception */ public function set_competency_filter( string $fieldlocator, string $value, ): void { $field = behat_field_manager::get_form_field_from_label($fieldlocator, $this)->get_node(); $session = $this->getSession(); $value = trim($value); // Click into the field. $field->click(); // Remove any existing text. do { behat_base::type_keys($session, [behat_keys::BACKSPACE, behat_keys::DELETE]); } while (strlen($field->getValue()) > 0); $this->wait_for_pending_js(); // Type in the new value. behat_base::type_keys($session, str_split($value)); $this->wait_for_pending_js(); // If the autocomplete found suggestions, then it will have: // 1) marked itself as expanded; and // 2) have an aria-selected suggestion in the list. $expanded = $field->getAttribute('aria-expanded'); $suggestion = $field->getParent()->getParent()->find('css', '.form-autocomplete-suggestions > [aria-selected="true"]'); if ($expanded && null !== $suggestion) { // A suggestion was found. // Click on the first item in the list. $suggestion->click(); } else { throw new \InvalidArgumentException( "Unable to find '{$value}' in the list of options." ); } $this->wait_for_pending_js(); } } boostnavbar_test.php000064400000052066152266371410010654 0ustar00. namespace theme_boost; /** * Test the boostnavbar file * * @package theme_boost * @covers \theme_boost\boostnavbar * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class boostnavbar_test extends \advanced_testcase { /** * Provider for test_remove_no_link_items * The setup and expected arrays are defined as an array of 'nodekey' => $hasaction * * @return array */ public static function remove_no_link_items_provider(): array { return [ 'All nodes have links links including leaf node. Set to remove section nodes.' => [ [ 'node1' => ['hasaction' => true, 'issection' => false], 'node2' => ['hasaction' => true, 'issection' => false], 'node3' => ['hasaction' => true, 'issection' => false], ], true, [ 'Home' => true, 'Courses' => true, 'tc_1' => true, 'node1' => true, 'node2' => true, 'node3' => true, ] ], 'Only some parent nodes have links. Leaf node has a link. Set to remove section nodes.' => [ [ 'node1' => ['hasaction' => false, 'issection' => false], 'node2' => ['hasaction' => true, 'issection' => false], 'node3' => ['hasaction' => true, 'issection' => false], ], true, [ 'Home' => true, 'Courses' => true, 'tc_1' => true, 'node2' => true, 'node3' => true, ] ], 'All parent nodes do not have links. Leaf node has a link. Set to remove section nodes.' => [ [ 'node1' => ['hasaction' => false, 'issection' => false], 'node2' => ['hasaction' => false, 'issection' => false], 'node3' => ['hasaction' => true, 'issection' => false], ], true, [ 'Home' => true, 'Courses' => true, 'tc_1' => true, 'node3' => true, ] ], 'All parent nodes have links. Leaf node does not has a link. Set to remove section nodes.' => [ [ 'node1' => ['hasaction' => true, 'issection' => false], 'node2' => ['hasaction' => true, 'issection' => false], 'node3' => ['hasaction' => false, 'issection' => false], ], true, [ 'Home' => true, 'Courses' => true, 'tc_1' => true, 'node1' => true, 'node2' => true, 'node3' => false, ] ], 'All parent nodes do not have links. Leaf node does not has a link. Set to remove section nodes.' => [ [ 'node1' => ['hasaction' => false, 'issection' => false], 'node2' => ['hasaction' => false, 'issection' => false], 'node3' => ['hasaction' => false, 'issection' => false], ], true, [ 'Home' => true, 'Courses' => true, 'tc_1' => true, 'node3' => false, ] ], 'Some parent nodes do not have links. Leaf node does not has a link. Set to remove section nodes.' => [ [ 'node1' => ['hasaction' => true, 'issection' => false], 'node2' => ['hasaction' => false, 'issection' => false], 'node3' => ['hasaction' => false, 'issection' => false], ], true, [ 'Home' => true, 'Courses' => true, 'tc_1' => true, 'node1' => true, 'node3' => false, ] ], 'All nodes have links links including leaf node and section nodes. Set to remove section nodes.' => [ [ 'node1' => ['hasaction' => true, 'issection' => false], 'node2' => ['hasaction' => true, 'issection' => false], 'sectionnode1' => ['hasaction' => true, 'issection' => true], 'node3' => ['hasaction' => true, 'issection' => false], ], true, [ 'Home' => true, 'Courses' => true, 'tc_1' => true, 'node1' => true, 'node2' => true, 'node3' => true, ] ], 'All nodes have links links including leaf node and section nodes. Set to not remove section nodes.' => [ [ 'node1' => ['hasaction' => true, 'issection' => false], 'node2' => ['hasaction' => true, 'issection' => false], 'sectionnode1' => ['hasaction' => true, 'issection' => true], 'node3' => ['hasaction' => true, 'issection' => false], ], false, [ 'Home' => true, 'Courses' => true, 'tc_1' => true, 'node1' => true, 'node2' => true, 'sectionnode1' => true, 'node3' => true, ] ], 'Only some parent nodes have links. Section node does not have a link. Set to not remove section nodes.' => [ [ 'node1' => ['hasaction' => false, 'issection' => false], 'node2' => ['hasaction' => true, 'issection' => false], 'sectionnode1' => ['hasaction' => false, 'issection' => true], 'node3' => ['hasaction' => true, 'issection' => false], ], true, [ 'Home' => true, 'Courses' => true, 'tc_1' => true, 'node2' => true, 'node3' => true, ] ] ]; } /** * Test the remove_no_link_items function * * @dataProvider remove_no_link_items_provider * @param array $setup * @param bool $removesectionnodes Whether to remove the section nodes with an associated action. * @param array $expected * @throws \ReflectionException */ public function test_remove_no_link_items(array $setup, bool $removesectionnodes, array $expected): void { global $PAGE; $this->resetAfterTest(); // Unfortunate hack needed because people use global $PAGE around the place. $PAGE->set_url('/'); $course = $this->getDataGenerator()->create_course(); $page = new \moodle_page(); $page->set_course($course); $page->set_url(new \moodle_url('/course/view.php', array('id' => $course->id))); // A dummy url to use. We don't care where it's pointing to. $url = new \moodle_url('/'); foreach ($setup as $key => $value) { $page->navbar->add($key, $value['hasaction'] ? $url : null, $value['issection'] ? \navigation_node::TYPE_SECTION : null); } $boostnavbar = $this->getMockBuilder(boostnavbar::class) ->disableOriginalConstructor() ->onlyMethods([]) ->getMock(); $rc = new \ReflectionClass(boostnavbar::class); $rcp = $rc->getProperty('items'); $rcp->setValue($boostnavbar, $page->navbar->get_items()); // Make the call to the function. $rcm = $rc->getMethod('remove_no_link_items'); $rcm->invoke($boostnavbar, $removesectionnodes); // Get the value for the class variable that the function modifies. $values = $rcp->getValue($boostnavbar); $actual = []; foreach ($values as $value) { $actual[$value->text] = $value->has_action(); } $this->assertEquals($expected, $actual); } /** * Provider for test_remove_duplicate_items. * * @return array */ public static function remove_duplicate_items_provider(): array { global $CFG; return [ 'Breadcrumb items with identical text and action url (actions of same type moodle_url).' => [ [ [ 'text' => 'Node 1', 'action' => new \moodle_url('/page1.php') ], [ 'text' => 'Node 2', 'action' => new \moodle_url('/page2.php', ['id' => 1]) ], [ 'text' => 'Node 4', 'action' => new \moodle_url('/page4.php', ['id' => 1]) ], [ 'text' => 'Node 2', 'action' => new \moodle_url('/page2.php', ['id' => 1]) ], ], ['Home', 'Node 1', 'Node 4', 'Node 2'] ], 'Breadcrumb items with identical text and action url (actions of different type moodle_url/text).' => [ [ [ 'text' => 'Node 1', 'action' => new \moodle_url('/page1.php') ], [ 'text' => 'Node 2', 'action' => new \moodle_url('/page2.php', ['id' => 1]) ], [ 'text' => 'Node 4', 'action' => new \moodle_url('/page4.php', ['id' => 1]) ], [ 'text' => 'Node 2', 'action' => "{$CFG->wwwroot}/page2.php?id=1" ], ], ['Home', 'Node 1', 'Node 4', 'Node 2'] ], 'Breadcrumb items with identical text and action url (actions of different type moodle_url/action_link).' => [ [ [ 'text' => 'Node 1', 'action' => new \moodle_url('/page1.php') ], [ 'text' => 'Node 2', 'action' => new \moodle_url('/page2.php', ['id' => 1]) ], [ 'text' => 'Node 4', 'action' => new \moodle_url('/page4.php', ['id' => 1]) ], [ 'text' => 'Node 2', 'action' => new \action_link(new \moodle_url('/page2.php', ['id' => 1]), 'Action link') ], ], ['Home', 'Node 1', 'Node 4', 'Node 2'] ], 'Breadcrumbs items with identical text but not identical action url.' => [ [ [ 'text' => 'Node 1', 'action' => new \moodle_url('/page1.php') ], [ 'text' => 'Node 2', 'action' => new \moodle_url('/page2.php', ['id' => 1]) ], [ 'text' => 'Node 2', 'action' => new \moodle_url('/page2.php', ['id' => 2]) ], [ 'text' => 'Node 4', 'action' => new \moodle_url('/page4.php', ['id' => 1]) ], ], ['Home', 'Node 1', 'Node 2', 'Node 2', 'Node 4'] ], 'Breadcrumb items with identical action url but not identical text.' => [ [ [ 'text' => 'Node 1', 'action' => new \moodle_url('/page1.php') ], [ 'text' => 'Node 2', 'action' => new \moodle_url('/page2.php', ['id' => 1]) ], [ 'text' => 'Node 3', 'action' => new \moodle_url('/page2.php', ['id' => 1]) ], [ 'text' => 'Node 4', 'action' => new \moodle_url('/page4.php', ['id' => 1]) ], ], ['Home', 'Node 1', 'Node 2', 'Node 3', 'Node 4'] ], 'Breadcrumb items without any identical action url or text.' => [ [ [ 'text' => 'Node 1', 'action' => new \moodle_url('/page1.php') ], [ 'text' => 'Node 2', 'action' => new \moodle_url('/page2.php', ['id' => 1]) ], [ 'text' => 'Node 3', 'action' => new \moodle_url('/page3.php', ['id' => 1]) ], [ 'text' => 'Node 4', 'action' => new \moodle_url('/page4.php', ['id' => 1]) ], ], ['Home', 'Node 1', 'Node 2', 'Node 3', 'Node 4'] ], ]; } /** * Test the remove_duplicate_items function. * * @dataProvider remove_duplicate_items_provider * @param array $navbarnodes The array containing the text and action of the nodes to be added to the navbar * @param array $expected The array containing the text of the expected navbar nodes */ public function test_remove_duplicate_items(array $navbarnodes, array $expected): void { $this->resetAfterTest(); $page = new \moodle_page(); $page->set_url('/'); // Add the navbar nodes. foreach ($navbarnodes as $node) { $page->navbar->add($node['text'], $node['action'], \navigation_node::TYPE_CUSTOM); } $boostnavbar = $this->getMockBuilder(boostnavbar::class) ->disableOriginalConstructor() ->onlyMethods([]) ->getMock(); $rc = new \ReflectionClass(boostnavbar::class); $rcp = $rc->getProperty('items'); $rcp->setValue($boostnavbar, $page->navbar->get_items()); // Make the call to the function. $rcm = $rc->getMethod('remove_duplicate_items'); $rcm->invoke($boostnavbar); // Get the value for the class variable that the function modifies. $values = $rcp->getValue($boostnavbar); $actual = []; foreach ($values as $value) { $actual[] = $value->text; } $this->assertEquals($expected, $actual); } /** * Provider for test_remove_items_that_exist_in_navigation. * * @return array */ public static function remove_items_that_exist_in_navigation_provider(): array { global $CFG; return [ 'Item with identical action url and text exists in the primary navigation menu.' => [ 'primary', [ [ 'text' => 'Node 1', 'action' => new \moodle_url('/page1.php') ], ], [ 'Node 1' => new \moodle_url('/page1.php'), 'Node 2' => new \moodle_url('/page2.php'), 'Node 3' => new \moodle_url('/page1.php'), ], ['Node 2', 'Node 3'] ], 'Item with identical action url and text exists in the secondary navigation menu.' => [ 'secondary', [ [ 'text' => 'Node 2', 'action' => new \moodle_url('/page2.php') ], ], [ 'Node 1' => new \moodle_url('/page1.php'), 'Node 2' => new \moodle_url('/page2.php'), 'Node 3' => new \moodle_url('/page1.php'), ], ['Home', 'Node 1', 'Node 3'] ], 'Multiple items with identical action url and text exist in the secondary navigation menu.' => [ 'secondary', [ [ 'text' => 'Node 2', 'action' => new \moodle_url('/page2.php') ], [ 'text' => 'Node 3', 'action' => new \moodle_url('/page3.php') ], ], [ 'Node 1' => new \moodle_url('/page1.php'), 'Node 2' => "{$CFG->wwwroot}/page2.php", 'Node 3' => new \action_link(new \moodle_url('/page3.php'), 'Action link'), ], ['Home', 'Node 1'] ], 'No items with identical action url and text in the secondary navigation menu.' => [ 'secondary', [ [ 'text' => 'Node 4', 'action' => new \moodle_url('/page4.php') ], ], [ 'Node 1' => new \moodle_url('/page1.php'), 'Node 2' => new \moodle_url('/page2.php'), 'Node 3' => new \moodle_url('/page1.php'), ], ['Home', 'Node 1', 'Node 2', 'Node 3'] ], ]; } /** * Test the remove_items_that_exist_in_navigation function. * * @dataProvider remove_items_that_exist_in_navigation_provider * @param string $navmenu The name of the navigation menu we would like to use (primary or secondary) * @param array $navmenunodes The array containing the text and action of the nodes to be added to the navigation menu * @param array $navbarnodes Array containing the text => action of the nodes to be added to the navbar * @param array $expected Array containing the text of the expected navbar nodes after the filtering */ public function test_remove_items_that_exist_in_navigation(string $navmenu, array $navmenunodes, array $navbarnodes, array $expected): void { global $PAGE; // Unfortunate hack needed because people use global $PAGE around the place. $PAGE->set_url('/'); $this->resetAfterTest(); $page = new \moodle_page(); $page->set_url('/'); switch ($navmenu) { case 'primary': $navigationmenu = new \core\navigation\views\primary($page); break; case 'secondary': $navigationmenu = new \core\navigation\views\secondary($page); } $navigationmenu->initialise(); // Add the additional nodes to the navigation menu. foreach ($navmenunodes as $navmenunode) { $navigationmenu->add($navmenunode['text'], $navmenunode['action'], \navigation_node::TYPE_CUSTOM); } // Add the additional navbar nodes. foreach ($navbarnodes as $text => $action) { $page->navbar->add($text, $action, \navigation_node::TYPE_CUSTOM); } $boostnavbar = $this->getMockBuilder(boostnavbar::class) ->disableOriginalConstructor() ->onlyMethods([]) ->getMock(); $rc = new \ReflectionClass(boostnavbar::class); $rcp = $rc->getProperty('items'); $rcp->setValue($boostnavbar, $page->navbar->get_items()); // Make the call to the function. $rcm = $rc->getMethod('remove_items_that_exist_in_navigation'); $rcm->invoke($boostnavbar, $navigationmenu); // Get the value for the class variable that the function modifies. $values = $rcp->getValue($boostnavbar); $actual = []; foreach ($values as $value) { $actual[] = $value->text; } $this->assertEquals($expected, $actual); } } scss_test.php000064400000002711152266371410007277 0ustar00. namespace theme_classic; /** * Unit tests for scss compilation. * * @package theme_classic * @category test * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class scss_test extends \advanced_testcase { /** * Test that classic can be compiled using SassC (the defacto implemention). */ public function test_scss_compilation_with_sassc(): void { if (!defined('PHPUNIT_PATH_TO_SASSC')) { $this->markTestSkipped('Path to SassC not provided'); } $this->resetAfterTest(); set_config('pathtosassc', PHPUNIT_PATH_TO_SASSC); $this->assertNotEmpty( \theme_config::load('classic')->get_css_content_debug('scss', null, null) ); } } behat/reset_tour.feature000064400000002270152266371410011407 0ustar00@tool @tool_usertours @theme_boost Feature: Reset a tour for Boost In order to test a tour As an administrator I can reset the tour to force it to display again Background: Given I log in as "admin" And I add a new user tour with: | Name | First tour | | Description | My first tour | | Apply to URL match | FRONTPAGE | | Tour is enabled | 1 | | Show with backdrop | 1 | And I add steps to the "First tour" tour: | targettype | Title | id_content | Content type | | Display in middle of page | Welcome | Welcome tour. | Manual | @javascript Scenario: Reset the tour with desktop view # Changing the window size to large so we will have the footer button. Given I change window size to "large" And I am on site homepage And I should see "Welcome" And I press "Got it" And I should not see "Welcome" When I click on ".btn-footer-popover" "css_element" in the "#page-footer" "css_element" Then I should see "Reset user tour on this page" And I click on "Reset user tour on this page" "link" And I should see "Welcome" behat/addblock.feature000064400000002541152266371410010760 0ustar00@javascript @theme_classic Feature: Add a block using classic theme In order to check the blocks to display in the Add a block list for a them As an administrator I need to confirm the unaddableblocks setting is empty for classic. Background: Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And I log in as "admin" Scenario: All the expected blocks are displayed in the Add a block list for classic Given I am on "Course 1" course homepage with editing mode on When I click on "Add a block" "select" Then I should see "Administration" And I should see "Navigation" And I should see "Courses" And I should see "Section links" Scenario: Admins can change unaddable blocks using the unaddableblocks setting for classic Given the following config values are set as admin: | unaddableblocks | online_users,private_files,settings | theme_classic| And I am on "Course 1" course homepage with editing mode on When I click on "Add a block" "select" Then I should not see "Online users" And I should not see "Private files" # The settings block is defined as required block for classic, so it will be displayed always. And I should see "Administration" And I should see "Navigation" And I should see "Courses" And I should see "Section links" behat/behat_theme_boost_behat_navigation.php000064400000010402152266371410015401 0ustar00. // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. // For that reason, we can't even rely on $CFG->admin being available here. require_once(__DIR__ . '/../../../../lib/tests/behat/behat_navigation.php'); use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; use Behat\Mink\Exception\ExpectationException as ExpectationException; /** * Step definitions related to the navigation in the Boost theme. * * @package theme_boost * @category test * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_theme_boost_behat_navigation extends behat_navigation { /** * Checks whether a node is active in the navbar. * * @override i should see :name is active in navigation * * @throws ElementNotFoundException * @param string $element The name of the nav elemnent to look for. * @return void */ public function i_should_see_is_active_in_navigation($element) { $this->execute("behat_general::assert_element_contains_text", [$element, '.navbar .nav-link.active', 'css_element']); } /** * Checks whether a node is active in the secondary nav. * * @Given i should see :name is active in secondary navigation * @throws ElementNotFoundException * @param string $element The name of the nav elemnent to look for. * @return void */ public function i_should_see_is_active_in_secondary_navigation($element) { $this->execute("behat_general::assert_element_contains_text", [$element, '.secondary-navigation .nav-link.active', 'css_element']); } /** * Checks whether the language selector menu is present in the navbar. * * @Given language selector menu should exist in the navbar * @Given language selector menu should :not exist in the navbar * * @throws ElementNotFoundException * @param string|null $not Instructs to checks whether the element does not exist in the user menu, if defined * @return void */ public function lang_menu_should_exist($not = null) { $callfunction = is_null($not) ? 'should_exist' : 'should_not_exist'; $this->execute("behat_general::{$callfunction}", [$this->get_lang_menu_xpath(), 'xpath_element']); } /** * Checks whether an item exists in the language selector menu. * * @Given :itemtext :selectortype should exist in the language selector menu * @Given :itemtext :selectortype should :not exist in the language selector menu * * @throws ElementNotFoundException * @param string $itemtext The menu item to find * @param string $selectortype The selector type * @param string|null $not Instructs to checks whether the element does not exist in the user menu, if defined * @return void */ public function should_exist_in_lang_menu($itemtext, $selectortype, $not = null) { $callfunction = is_null($not) ? 'should_exist_in_the' : 'should_not_exist_in_the'; $this->execute("behat_general::{$callfunction}", [$itemtext, $selectortype, $this->get_lang_menu_xpath(), 'xpath_element']); } /** * Return the xpath for the language selector menu element. * * @return string The xpath */ protected function get_lang_menu_xpath() { return "//nav[contains(concat(' ', @class, ' '), ' navbar ')]" . "//div[contains(concat(' ', @class, ' '), ' langmenu ')]" . "//div[contains(concat(' ', @class, ' '), ' dropdown-menu ')]"; } } behat/welcome_to_moodle.feature000064400000003417152266371410012714 0ustar00@javascript @theme_boost Feature: Welcome message on boost To be welcome in moodle As a User I need to see a welcome message on the first page @accessibility Scenario: Login and be welcomed on the homepage Given the following config values are set as admin: | defaulthomepage | 0 | When I log in as "admin" Then I should see "Acceptance test site" in the "page-header" "region" And I should see "Welcome, Admin!" in the "page-header" "region" And I reload the page And I should not see "Welcome, Admin!" in the "page-header" "region" And I should see "Acceptance test site" in the "page-header" "region" And the page should meet accessibility standards @accessibility Scenario: Login and be welcomed on the dashboard Given the following config values are set as admin: | defaulthomepage | 1 | When I log in as "admin" Then I should see "Dashboard" in the "page-header" "region" And I should see "Welcome, Admin!" in the "page-header" "region" And I reload the page And I should not see "Welcome, Admin!" in the "page-header" "region" And I should see "Dashboard" in the "page-header" "region" And the page should meet accessibility standards with "best-practice" extra tests @accessibility Scenario: Login and be welcomed on the my courses page Given the following config values are set as admin: | defaulthomepage | 3 | When I log in as "admin" Then I should see "My courses" in the "page-header" "region" And I should see "Welcome, Admin!" in the "page-header" "region" And I reload the page And I should not see "Welcome, Admin!" in the "page-header" "region" And I should see "My courses" in the "page-header" "region" And the page should meet accessibility standards behat/theme_selector.feature000064400000002637152266371410012225 0ustar00@core @core_admin @theme_classic Feature: Select a theme in Classic theme In order to choose a theme As an admin I need to preview the theme and make a selection Background: Given I log in as "admin" And I navigate to "Appearance > Themes" in site administration @javascript Scenario: I am able to preview a theme using a modal window When I click on "Preview theme 'Classic'" "button" Then I should see "Classic" in the "Classic" "dialogue" And I should see "Classic is a highly-customisable theme," in the "Classic" "dialogue" And I should see "Current theme" in the "Classic" "dialogue" @javascript Scenario: I am able to change the theme using the modal window select button Given I should see "Current theme" in the "#theme-card-classic" "css_element" When I click on "Preview theme 'Boost'" "button" And I click on "Select theme" "button" in the "Boost" "dialogue" Then I should see "Current theme" in the "#theme-card-boost" "css_element" Scenario: I am able to change the theme using the normal select button Given I should see "Current theme" in the "#theme-card-classic" "css_element" When I click on "Select theme 'Boost'" "button" Then I should see "Current theme" in the "#theme-card-boost" "css_element" Scenario: I am able to view a theme's settings page Given I click on "Edit theme settings 'Classic'" "link" Then I should see "Classic" behat/tour_filter.feature000064400000004017152266371410011553 0ustar00@tool @tool_usertours @theme_boost Feature: Apply tour filters to a tour for Classic In order to give more directed tours As an administrator I need to create a user tour specific to theme Classic @javascript Scenario: Add a tour for theme Classic Given I log in as "admin" And I add a new user tour with: | Name | First tour | | Description | My first tour | | Apply to URL match | /my/% | | Tour is enabled | 1 | | Theme | Classic | And I add steps to the "First tour" tour: | targettype | Title | id_content | Content type | | Display in middle of page | Welcome | Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful | Manual | When I am on homepage Then I should not see "Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful" @javascript Scenario: Add a tour for theme Boost Given I log in as "admin" And I add a new user tour with: | Name | First tour | | Description | My first tour | | Apply to URL match | /my/% | | Tour is enabled | 1 | | Theme | Boost | And I add steps to the "First tour" tour: | targettype | Title | id_content | Content type | | Display in middle of page | Welcome | Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful | Manual | When I am on homepage Then I should see "Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful" behat/mycoursesblocks.feature000064400000002327152266371410012446 0ustar00@javascript @theme_classic Feature: My courses page block layout in Classic theme In order to have a clear and consistent view on the my courses page As a student I need to see the blocks in the expected placement Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student@example.com | And I log in as "admin" And I am on site homepage And I turn editing mode on And I add the "Text" block And I configure the "(new text block)" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | | Text block title | Text on all pages | | Content | This is visible on all pages | | Default region | Right | And I press "Save changes" Scenario: Student can see relevant blocks with correct placement on my courses page When I log in as "student1" And I am on the "My courses" page Then "Course overview" "text" should exist in the "region-main" "region" And "This is visible on all pages" "text" should exist in the ".columnright" "css_element" behat/course_sections.feature000064400000002325152266371410012424 0ustar00@theme_classic Feature: Select course sections using classic theme In order to view course sections when using the classic theme As a teacher I need to select the section from the section selector Background: Given the following "course" exists: | fullname | Course 1 | | shortname | C1 | | numsections | 3 | | initsections | 1 | And the following "activities" exist: | course | activity | name | idnumber | section | | C1 | assign | Assignment 1 | assign1 | 1 | | C1 | assign | Assignment 2 | assign2 | 2 | @javascript Scenario: Use the course section selector in classic theme Given I am on the "C1" "Course" page logged in as "admin" And I turn editing mode on When I choose the "View" item in the "Edit" action menu of the "Section 1" "section" Then I should see "Section 1" And I should see "Assignment 1" And I should not see "Assignment 2" And I select "Section 2" from the "jump" singleselect And I should see "Section 2" And I should not see "Assignment 1" And I should see "Assignment 2" And the "jump" select box should contain "Section 3" behat/formerrors.feature000064400000002116152266371410011413 0ustar00@javascript @theme_boost Feature: Form errors only display after submit or change in Boost theme In order to have a more accessible way to display form errors As anyone I need to see errors only after I submit the form or change a field Background: Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | Scenario: Form error appears after submit, but not after just tabbing through When I log in as "admin" And I navigate to "Courses > Add a new course" in site administration And I click on "Course full name" "field" And I press the tab key Then I should not see "Missing full name" And I press "Save and display" And I should see "Missing full name" And the focused element is "Course full name" "field" Scenario: Form error appears immediately after change When I am on the "C1" "course editing" page logged in as "admin" And I set the field "Course full name" to "" And I press the tab key Then I should see "Missing full name" And the focused element is "Course full name" "field" behat/blacklist.json000064400000001431152266371410010500 0ustar00{ "contexts": [ "behat_theme_boost_behat_navigation" ], "features": [ "lib/tests/behat/action_menu.feature", "admin/tests/behat/site_administration.feature", "blocks/tests/behat/hide_blocks.feature", "blocks/tests/behat/move_blocks.feature", "course/format/tests/behat/course_courseindex.feature", "course/format/tests/behat/courseindex_keyboardnav.feature", "course/format/tests/behat/courseindex_completion.feature", "course/format/tests/behat/courseindex_language.feature", "badges/tests/behat/badge_navigation.feature", "lib/tests/behat/menu_navigation_single_activity.feature", "lib/tests/behat/menu_navigation_topics.feature" ], "tags": [ "addablocklink" ] } behat/course_reuse.feature000064400000004070152266371410011717 0ustar00@javascript @theme_boost Feature: Course reuse navigation As a teacher I can navigate to course reuse pages Background: Given the following "courses" exist: | fullname | shortname | newsitems | | Course 1 | C1 | 5 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | Scenario: A Teacher can navigate to the course Import page. Given I log in as "teacher1" When I am on "Course 1" course homepage And I navigate to "Course reuse" in current page administration Then I should not see "Find a course to import data from:" And I should see "Import" And I should see "Backup" And I should see "Restore" And I should not see "Copy course" Scenario Outline: A Teacher can navigate to other Course reuse pages. Given I log in as "teacher1" When I am on "Course 1" course homepage And I navigate to "Course reuse" in current page administration And I follow "" Then I should see "" Examples: | adminpage | content | | Backup | Backup settings | | Restore | Upload a backup file | | Import | Find a course to import data from: | | Reset | This feature allows you to clear all user data and reset the course to its original state | Scenario: An Administrator can view the course copy page. Given I log in as "admin" When I am on "Course 1" course homepage And I navigate to "Course reuse" in current page administration And I follow "Copy course" Then I should see "Create a copy of this course in any course category" behat/settingstabs.feature000064400000001664152266371410011734 0ustar00@javascript @theme_boost Feature: Administration nav tabs Scenario: See last opened tab in site admin when returning to the page Given I log in as "admin" And I am on site homepage And I click on "Site administration" "link" And I click on "Users" "link" And I click on "Browse list of users" "link" And I should see "Add a new user" When I press the "back" button in the browser Then I should see "Cohorts" Scenario: Navigate back to specific tab after search Given I log in as "admin" And I am on site homepage And I click on "Site administration" "link" And I set the field "Search" to "assignment" And I press "Search" # I should be redirected to the site admin tab with the complete list under it. # Testing the existence of at least one of the options in the node is sufficient. When I select "Users" from secondary navigation Then I should see "Browse list of users" behat/primarynav.feature000064400000013231152266371410011403 0ustar00@javascript @theme_boost Feature: Primary navigation To navigate in boost theme As a user I need to use the primary navigation Background: Given the following "users" exist: | username | firstname | lastname | email | | user1 | User | One | user1@example.com | @javascript @theme_boost Scenario Outline: Admin sets defaulthomepage and verify the landing page and site home link Given I log in as "admin" And the following config values are set as admin: | defaulthomepage | | And I am on homepage And I should see "" in the "a.nav-link.active:not([tabindex])" "css_element" And I should see "" in the "" "css_element" Examples: | defaulthomepageset | homepage | sitehome | linkelement | | 0 | Home | Home | a.nav-link.active:not([tabindex]):not([href*='redirect=0']) | | 1 | Dashboard | Home | a.nav-link[tabindex='-1'][href$='redirect=0'] | | 3 | My courses | Home | a.nav-link[tabindex='-1'][href$='redirect=0'] | @javascript @theme_boost Scenario Outline: Admin sets defaulthomepage to user preference and verifies the landing page based on it Given I log in as "admin" And I navigate to "Appearance > Navigation" in site administration And I set the field "Start page for users" to "User preference" And I press "Save changes" And I follow "Preferences" in the user menu And I follow "Start page" And I set the field "Start page" to "" And I press "Save changes" And the following config values are set as admin: | defaulthomepage | 2 | And I log out And I log in as "admin" And I should see "" in the "a.nav-link.active:not([tabindex])" "css_element" Examples: | userpreference | homepage | | Home | Home | | Dashboard | Dashboard | | My courses | My courses | @javascript @theme_boost Scenario: Users could use primary nav menu on mobile size screens Given I change window size to "mobile" And I am on the "My courses" page logged in as "user1" Then "Home" "link" should not be visible And "Side panel" "button" should exist And I click on "Side panel" "button" And I should see "Home" in the "theme_boost-drawers-primary" "region" And I click on "[data-region='site-home-link']" "css_element" And I should see "Dashboard" in the "page-header" "region" @theme_boost Scenario: Guest users can only see the Home and Calendar items in the primary navigation menu Given I log in as "guest" When I am on site homepage Then I should see "Home" in the ".primary-navigation" "css_element" And I should see "Calendar" in the ".primary-navigation" "css_element" And I should not see "Dashboard" in the ".primary-navigation" "css_element" And I should not see "My courses" in the ".primary-navigation" "css_element" And I should not see "Site administration" in the ".primary-navigation" "css_element" Scenario: Dashboard is not displayed in the primary navigation when it is disabled Given the following config values are set as admin: | enabledashboard | 0 | When I am on the "My courses" page logged in as "user1" Then I should not see "Dashboard" And the following config values are set as admin: | enabledashboard | 1 | # We need to reload the page to skip the "Welcome, xxxx!" and display the real page title. And I reload the page And I should see "Dashboard" Scenario: Start page when default home is dashboard but dashboard is disabled Given the following config values are set as admin: | enabledashboard | 0 | # 1 = Dashboard. | defaulthomepage | 1 | When I log in as "admin" # We need to reload the page to skip the "Welcome, xxxx!" and display the real page title. And I reload the page Then I should not see "Dashboard" in the "page-header" "region" And I should see "My courses" in the "page-header" "region" And I log out # Check dashboard is displayed when it's re-enabled. And the following config values are set as admin: | enabledashboard | 1 | And I log in as "admin" # We need to reload the page to skip the "Welcome, xxxx!" and display the real page title. And I reload the page And I should see "Dashboard" in the "page-header" "region" And I should not see "My courses" in the "page-header" "region" Scenario: Start page when default home is user preference set to dashboard but dashboard is disabled Given the following config values are set as admin: | enabledashboard | 0 | # 2 = User preference. | defaulthomepage | 2 | # 1 = Dashboard. And the following "user preferences" exist: | user | preference | value | | admin | user_home_page_preference | 1 | When I log in as "admin" # We need to reload the page to skip the "Welcome, xxxx!" and display the real page title. And I reload the page Then I should not see "Dashboard" And I should see "My courses" in the "page-header" "region" And I log out # Check dashboard is displayed when it's re-enabled. And the following config values are set as admin: | enabledashboard | 1 | And I log in as "admin" # We need to reload the page to skip the "Welcome, xxxx!" and display the real page title. And I reload the page And I should see "Dashboard" in the "page-header" "region" And I should not see "My courses" in the "page-header" "region" behat/regionmainsettingsmenu.feature000064400000002325152266371410014013 0ustar00@javascript @theme_boost Feature: Region main settings menu To navigate in boost theme I need to use the region main settings menu Background: Given the following "courses" exist: | fullname | shortname | newsitems | | Course 1 | C1 | 5 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | option | | choice | Choice name | Test choice description | C1 | choice1 | Option 1, Option 2, Option 3 | Scenario: Student cannot use all options in the region main settings menu Given I log in as "student1" When I am on "Course 1" course homepage Then "#region-main-settings-menu [role=button]" "css_element" should not exist And I am on the "Choice name" "Choice activity" page And "#region-main-settings-menu [role=button]" "css_element" should not exist behat/category_role_assignment.feature000064400000003145152266371410014304 0ustar00@core @core_course @theme_classic Feature: Role assignments can be made at the category level in classic In order to grant a user different capabilities As a user I can assign roles in categories Background: Given the following "users" exist: | username | firstname | lastname | | manager | Manager | Manager | And the following "categories" exist: | name | category | idnumber | | Cat 1 | 0 | CAT1 | And the following "role assigns" exist: | user | role | contextlevel | reference | | manager | manager | Category | CAT1 | And I log in as "admin" @javascript Scenario: A user with a category role can assign roles Given I define the allowed role assignments for the "Manager" role as: | Teacher | Assignable | And I log out And I log in as "manager" And I am on course index When I follow "Cat 1" Then "Assign roles" "link" should exist in current page administration @javascript Scenario: A user with a category role cannot assign roles if there are no roles to assign Given I define the allowed role assignments for the "Manager" role as: | Manager | Not assignable | | Course creator | Not assignable | | Teacher | Not assignable | | Non-editing teacher | Not assignable | | Student | Not assignable | And I change window size to "large" And I log out And I log in as "manager" And I am on course index When I follow "Cat 1" Then "Assign roles" "link" should not exist in current page administration behat/languagemenu.feature000064400000007576152266371410011702 0ustar00@javascript @theme_boost Feature: Language selector menu To be able to set the preferred language for the site As a user I need to be presented with a language selector menu Background: Given remote langimport tests are enabled And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "language pack" exists: | language | en_ar | Scenario: Logged user is presented with a language selector which is placed within the user menu Given I log in as "teacher1" And I am on site homepage # The language selector menu is not present in the navbar when a user is logged in. And language selector menu should not exist in the navbar # The language selector is present within the user menu. And "Language" "link" should exist in the user menu When I follow "Language" in the user menu Then I should see "Language selector" user submenu And "English ‎(en)‎" "link" should exist in the "Language selector" user submenu And "English (pirate) ‎(en_ar)‎" "link" should exist in the "Language selector" user submenu Scenario: Non-logged user is presented with a language selector which is placed within the navbar Given I am on site homepage # The language selector menu is present in the navbar when a user is not logged in. And language selector menu should exist in the navbar And "English ‎(en)‎" "link" should exist in the language selector menu And "English (pirate) ‎(en_ar)‎" "link" should exist in the language selector menu Scenario: Logged user is not presented with a language selector in a course if a language is forced in that context Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to "Settings" in current page administration And I expand all fieldsets And I set the following fields to these values: | id_lang | en | And I press "Save and display" # The language selector is not present within the user menu in the course context when a language is enforced. When I am on "Course 1" course homepage And "Language" "link" should not exist in the user menu # The language selector is present within the user menu in other contexts. And I am on site homepage And "Language" "link" should exist in the user menu Scenario: Logged user is not presented with a language selector if there is less than two installed languages Given I log in as "admin" And I navigate to "Language > Language packs" in site administration And I set the field "Installed language packs" to "en_ar" And I press "Uninstall selected language pack(s)" And I click on "Yes" "button" in the "Uninstall selected language pack(s)" "dialogue" And the "Installed language packs" select box should not contain "en_ar" When I am on site homepage # The language selector is not present within the user menu. And "Language" "link" should not exist in the user menu Scenario: Non-logged user is not presented with a language selector if there is less than two installed languages Given I log in as "admin" And I navigate to "Language > Language packs" in site administration And I set the field "Installed language packs" to "en_ar" And I press "Uninstall selected language pack(s)" And I click on "Yes" "button" in the "Uninstall selected language pack(s)" "dialogue" And the "Installed language packs" select box should not contain "en_ar" And I log out When I am on site homepage # The language selector menu is not present in the navbar. Then language selector menu should not exist in the navbar behat/contextmenu.feature000064400000002077152266371410011572 0ustar00@javascript @theme_boost Feature: Context settings menu To navigate in boost theme I need to use the context settings menu Background: Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | Scenario: Teacher can use the context settings menu And I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to "Settings" in current page administration And I should see "Edit course settings" And I log out Scenario: Student cannot use the context settings menu And I log in as "student1" And I am on "Course 1" course homepage And ".context-header-settings-menu [role=button]" "css_element" should not exist And I log out behat/breadcrumb.feature000064400000010622152266371410011322 0ustar00@javascript @theme_boost Feature: Breadcrumbs navigation To navigate in boost theme As an admin user I should see breadcrumbs Scenario: Admin user navigates to site administrations plugins assignment settings Given I log in as "admin" When I navigate to "Plugins > Activity modules > Assignment > Assignment settings" in site administration Then I should see "Activity modules" in the ".breadcrumb" "css_element" And I should see "Assignment" in the ".breadcrumb" "css_element" And I should see "Assignment settings" in the ".breadcrumb" "css_element" Scenario: Admin user navigates to site adminsitrations plugins assignment feedback offline grading worksheet Given I log in as "admin" When I navigate to "Plugins > Activity modules > Assignment > Feedback plugins > Offline grading worksheet" in site administration Then I should see "Activity modules" in the ".breadcrumb" "css_element" And I should see "Assignment" in the ".breadcrumb" "css_element" And I should see "Feedback plugins" in the ".breadcrumb" "css_element" And I should see "Offline grading worksheet" in the ".breadcrumb" "css_element" Scenario: Admin user navigates to site adminsitrations plugins badges manage backpacks page Given I log in as "admin" When I navigate to "Badges > Manage backpacks" in site administration Then I should see "Badges" in the ".breadcrumb" "css_element" And I should see "Manage backpacks" in the ".breadcrumb" "css_element" Scenario: Admin user changes the default home page and navigates to 'course category management' page Given the following config values are set as admin: | defaulthomepage | 3 | And the following "categories" exist: | name | category | idnumber | | Cat 1 | 0 | CAT1 | And I log in as "admin" And I navigate to "Courses > Manage courses and categories" in site administration When I follow "Cat 1" Then I should not see "My courses" in the ".breadcrumb" "css_element" And I should see "Cat 1" in the ".breadcrumb" "css_element" And I should see "Manage courses and categories" in the ".breadcrumb" "css_element" Scenario: Admin user sets the default home page to 'Site' and navigates to its 'Preferences' and 'Private files' page Given the following config values are set as admin: | defaulthomepage | 0 | And I log in as "admin" When I follow "Preferences" in the user menu # There should be no breadcrumbs on this page. Then ".breadcrumb-item" "css_element" should not exist in the ".breadcrumb" "css_element" Scenario: Admin user sets the default home page to 'Dashboard' and navigates to its 'Preferences' and 'Private files' page Given the following config values are set as admin: | defaulthomepage | 1 | And I log in as "admin" When I follow "Preferences" in the user menu # There should be no breadcrumbs on this page. Then ".breadcrumb-item" "css_element" should not exist in the ".breadcrumb" "css_element" And I follow "Private files" in the user menu # There should be no breadcrumbs on this page. And ".breadcrumb-item" "css_element" should not exist in the ".breadcrumb" "css_element" Scenario: Admin user sets the default home page to 'User preference' and navigates to its 'Preferences' and 'Private files' page Given the following config values are set as admin: | defaulthomepage | 2 | And I log in as "admin" When I follow "Preferences" in the user menu # There should be no breadcrumbs on this page. Then ".breadcrumb-item" "css_element" should not exist in the ".breadcrumb" "css_element" And I follow "Private files" in the user menu # There should be no breadcrumbs on this page. And ".breadcrumb-item" "css_element" should not exist in the ".breadcrumb" "css_element" Scenario: Admin user sets the default home page to 'My courses' and navigates to its 'Preferences' and 'Private files' page Given the following config values are set as admin: | defaulthomepage | 3 | And I log in as "admin" When I follow "Preferences" in the user menu # There should be no breadcrumbs on this page. Then ".breadcrumb-item" "css_element" should not exist in the ".breadcrumb" "css_element" And I follow "Private files" in the user menu # There should be no breadcrumbs on this page. And ".breadcrumb-item" "css_element" should not exist in the ".breadcrumb" "css_element" player_test.php000064400000013306152266372370007630 0ustar00. namespace media_html5video; use core_media_manager; use media_html5video_plugin; /** * Test script for media embedding. * * @package media_html5video * @copyright 2016 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class player_test extends \advanced_testcase { /** * Pre-test setup. Preserves $CFG. */ public function setUp(): void { parent::setUp(); // Reset $CFG and $SERVER. $this->resetAfterTest(); // Consistent initial setup: all players disabled. \core\plugininfo\media::set_enabled_plugins('html5video'); // Pretend to be using Firefox browser (must support ogg for tests to work). \core_useragent::instance(true, 'Mozilla/5.0 (X11; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0 '); } /** * Test that plugin is returned as enabled media plugin. */ public function test_is_installed(): void { $sortorder = \core\plugininfo\media::get_enabled_plugins(); $this->assertEquals(['html5video' => 'html5video'], $sortorder); } /** * Test method get_supported_extensions() */ public function test_get_supported_extensions(): void { $nativeextensions = file_get_typegroup('extension', 'html_video'); // Make sure that the list of extensions from the setting is exactly the same as html_video group. $player = new media_html5video_plugin(); $this->assertEmpty(array_diff($player->get_supported_extensions(), $nativeextensions)); $this->assertEmpty(array_diff($nativeextensions, $player->get_supported_extensions())); } /** * Test method list_supported_urls() */ public function test_list_supported_urls(): void { global $CFG; require_once($CFG->libdir . '/filelib.php'); $nativeextensions = file_get_typegroup('extension', 'html_video'); // Create list of URLs for each extension. $urls = array_map(function($ext){ return new \moodle_url('http://example.org/video.' . $ext); }, $nativeextensions); // Make sure that the list of supported URLs is not filtering permitted extensions. $player = new media_html5video_plugin(); $this->assertCount(count($urls), $player->list_supported_urls($urls)); } /** * Test embedding without media filter (for example for displaying file resorce). */ public function test_embed_url(): void { global $CFG; $url = new \moodle_url('http://example.org/1.webm'); $manager = core_media_manager::instance(); $embedoptions = array( core_media_manager::OPTION_TRUSTED => true, core_media_manager::OPTION_BLOCK => true, ); $this->assertTrue($manager->can_embed_url($url, $embedoptions)); $content = $manager->embed_url($url, 'Test & file', 0, 0, $embedoptions); $this->assertMatchesRegularExpression('~mediaplugin_html5video~', $content); $this->assertMatchesRegularExpression('~~', $content); $this->assertMatchesRegularExpression('~title="Test & file"~', $content); $this->assertMatchesRegularExpression('~width="' . $CFG->media_default_width . '"~', $content); $this->assertDoesNotMatchRegularExpression('~height=~', $content); // Allow to set automatic height. // Repeat sending the specific size to the manager. $content = $manager->embed_url($url, 'New file', 123, 50, $embedoptions); $this->assertMatchesRegularExpression('~width="123" height="50"~', $content); } /** * Test that mediaplugin filter replaces a link to the supported file with media tag. * * filter_mediaplugin is enabled by default. */ public function test_embed_link(): void { global $CFG; $url = new \moodle_url('http://example.org/some_filename.mp4'); $text = \html_writer::link($url, 'Watch this one'); $content = format_text($text, FORMAT_HTML); $this->assertMatchesRegularExpression('~mediaplugin_html5video~', $content); $this->assertMatchesRegularExpression('~~', $content); $this->assertMatchesRegularExpression('~title="Watch this one"~', $content); $this->assertDoesNotMatchRegularExpression('~assertMatchesRegularExpression('~width="' . $CFG->media_default_width . '"~', $content); } /** * Test that mediaplugin filter does not work on