MDL-64958 gradeimport_csv: Better unit tests for check_user_exists()
[moodle.git] / grade / import / csv / tests / load_data_test.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Unit tests for the class in load_data.php
19  *
20  * @package    gradeimport_csv
21  * @category   phpunit
22  * @copyright  2014 Adrian Greeve
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 global $CFG;
29 require_once($CFG->dirroot . '/grade/import/csv/tests/fixtures/phpunit_gradeimport_csv_load_data.php');
30 require_once($CFG->libdir . '/csvlib.class.php');
31 require_once($CFG->libdir . '/grade/grade_item.php');
32 require_once($CFG->libdir . '/grade/tests/fixtures/lib.php');
34 /**
35  * Unit tests for lib.php
36  *
37  * @package    gradeimport_csv
38  * @copyright  2014 Adrian Greeve
39  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class gradeimport_csv_load_data_testcase extends grade_base_testcase {
43     /** @var string $oktext Text to be imported. This data should have no issues being imported. */
44     protected $oktext = '"First name",Surname,"ID number",Institution,Department,"Email address","Assignment: Assignment for grape group", "Feedback: Assignment for grape group","Assignment: Second new grade item","Course total"
45 Anne,Able,,"Moodle HQ","Rock on!",student7@example.com,56.00,"We welcome feedback",,56.00
46 Bobby,Bunce,,"Moodle HQ","Rock on!",student5@example.com,75.00,,45.0,75.00';
48     /** @var string $badtext Text to be imported. This data has an extra column and should not succeed in being imported. */
49     protected $badtext = '"First name",Surname,"ID number",Institution,Department,"Email address","Assignment: Assignment for grape group","Course total"
50 Anne,Able,,"Moodle HQ","Rock on!",student7@example.com,56.00,56.00,78.00
51 Bobby,Bunce,,"Moodle HQ","Rock on!",student5@example.com,75.00,75.00';
53     /** @var string $csvtext CSV data to be imported with Last download from this course column. */
54     protected $csvtext = '"First name",Surname,"ID number",Institution,Department,"Email address","Assignment: Assignment for grape group", "Feedback: Assignment for grape group","Course total","Last downloaded from this course"
55 Anne,Able,,"Moodle HQ","Rock on!",student7@example.com,56.00,"We welcome feedback",56.00,{exportdate}
56 Bobby,Bunce,,"Moodle HQ","Rock on!",student5@example.com,75.00,,75.00,{exportdate}';
58     /** @var int $iid Import ID. */
59     protected $iid;
61     /** @var object $csvimport a csv_import_reader object that handles the csv import. */
62     protected $csvimport;
64     /** @var array $columns The first row of the csv file. These are the columns of the import file.*/
65     protected $columns;
67     public function tearDown() {
68         $this->csvimport = null;
69     }
71     /**
72      * Load up the above text through the csv import.
73      *
74      * @param string $content Text to be imported into the gradebook.
75      * @return array All text separated by commas now in an array.
76      */
77     protected function csv_load($content) {
78         // Import the csv strings.
79         $this->iid = csv_import_reader::get_new_iid('grade');
80         $this->csvimport = new csv_import_reader($this->iid, 'grade');
82         $this->csvimport->load_csv_content($content, 'utf8', 'comma');
83         $this->columns = $this->csvimport->get_columns();
85         $this->csvimport->init();
86         while ($line = $this->csvimport->next()) {
87             $testarray[] = $line;
88         }
90         return $testarray;
91     }
93     /**
94      * Test loading data and returning preview content.
95      */
96     public function test_load_csv_content() {
97         $encoding = 'utf8';
98         $separator = 'comma';
99         $previewrows = 5;
100         $csvpreview = new phpunit_gradeimport_csv_load_data();
101         $csvpreview->load_csv_content($this->oktext, $encoding, $separator, $previewrows);
103         $expecteddata = array(array(
104                 'Anne',
105                 'Able',
106                 '',
107                 'Moodle HQ',
108                 'Rock on!',
109                 'student7@example.com',
110                 56.00,
111                 'We welcome feedback',
112                 '',
113                 56.00
114             ),
115             array(
116                 'Bobby',
117                 'Bunce',
118                 '',
119                 'Moodle HQ',
120                 'Rock on!',
121                 'student5@example.com',
122                 75.00,
123                 '',
124                 45.0,
125                 75.00
126             )
127         );
129         $expectedheaders = array(
130             'First name',
131             'Surname',
132             'ID number',
133             'Institution',
134             'Department',
135             'Email address',
136             'Assignment: Assignment for grape group',
137             'Feedback: Assignment for grape group',
138             'Assignment: Second new grade item',
139             'Course total'
140         );
141         // Check that general data is returned as expected.
142         $this->assertEquals($csvpreview->get_previewdata(), $expecteddata);
143         // Check that headers are returned as expected.
144         $this->assertEquals($csvpreview->get_headers(), $expectedheaders);
146         // Check that errors are being recorded.
147         $csvpreview = new phpunit_gradeimport_csv_load_data();
148         $csvpreview->load_csv_content($this->badtext, $encoding, $separator, $previewrows);
149         // Columns shouldn't match.
150         $this->assertEquals($csvpreview->get_error(), get_string('csvweirdcolumns', 'error'));
151     }
153     /**
154      * Test fetching grade items for the course.
155      */
156     public function test_fetch_grade_items() {
158         $gradeitemsarray = grade_item::fetch_all(array('courseid' => $this->courseid));
159         $gradeitems = phpunit_gradeimport_csv_load_data::fetch_grade_items($this->courseid);
161         // Make sure that each grade item is located in the gradeitemsarray.
162         foreach ($gradeitems as $key => $gradeitem) {
163             $this->assertArrayHasKey($key, $gradeitemsarray);
164         }
166         // Get the key for a specific grade item.
167         $quizkey = null;
168         foreach ($gradeitemsarray as $key => $value) {
169             if ($value->itemname == "Quiz grade item") {
170                 $quizkey = $key;
171             }
172         }
174         // Expected modified item name.
175         $testitemname = get_string('modulename', $gradeitemsarray[$quizkey]->itemmodule) . ': ' .
176                 $gradeitemsarray[$quizkey]->itemname;
177         // Check that an item that is a module, is concatenated properly.
178         $this->assertEquals($testitemname, $gradeitems[$quizkey]);
179     }
181     /**
182      * Test the inserting of grade record data.
183      */
184     public function test_insert_grade_record() {
185         global $DB, $USER;
187         $user = $this->getDataGenerator()->create_user();
188         $this->setAdminUser();
190         $record = new stdClass();
191         $record->itemid = 4;
192         $record->newgradeitem = 25;
193         $record->finalgrade = 62.00;
194         $record->feedback = 'Some test feedback';
196         $testobject = new phpunit_gradeimport_csv_load_data();
197         $testobject->test_insert_grade_record($record, $user->id);
199         $gradeimportvalues = $DB->get_records('grade_import_values');
200         // Get the insert id.
201         $key = key($gradeimportvalues);
203         $testarray = array();
204         $testarray[$key] = new stdClass();
205         $testarray[$key]->id = $key;
206         $testarray[$key]->itemid = $record->itemid;
207         $testarray[$key]->newgradeitem = $record->newgradeitem;
208         $testarray[$key]->userid = $user->id;
209         $testarray[$key]->finalgrade = $record->finalgrade;
210         $testarray[$key]->feedback = $record->feedback;
211         $testarray[$key]->importcode = $testobject->get_importcode();
212         $testarray[$key]->importer = $USER->id;
213         $testarray[$key]->importonlyfeedback = 0;
215         // Check that the record was inserted into the database.
216         $this->assertEquals($gradeimportvalues, $testarray);
217     }
219     /**
220      * Test preparing a new grade item for import into the gradebook.
221      */
222     public function test_import_new_grade_item() {
223         global $DB;
225         $this->setAdminUser();
226         $this->csv_load($this->oktext);
227         $columns = $this->columns;
229         // The assignment is item 6.
230         $key = 6;
231         $testobject = new phpunit_gradeimport_csv_load_data();
233         // Key for this assessment.
234         $this->csvimport->init();
235         $testarray = array();
236         while ($line = $this->csvimport->next()) {
237             $testarray[] = $testobject->test_import_new_grade_item($columns, $key, $line[$key]);
238         }
240         // Query the database and check how many results were inserted.
241         $newgradeimportitems = $DB->get_records('grade_import_newitem');
242         $this->assertEquals(count($testarray), count($newgradeimportitems));
243     }
245     /**
246      * Data provider for \gradeimport_csv_load_data_testcase::test_check_user_exists().
247      *
248      * @return array
249      */
250     public function check_user_exists_provider() {
251         return [
252             'Fetch by email' => [
253                 'email', 's1@example.com', true
254             ],
255             'Fetch data using a non-existent email' => [
256                 'email', 's2@example.com', false
257             ],
258             'Multiple accounts with the same email' => [
259                 'email', 's1@example.com', false, 1
260             ],
261             'Fetch data using a valid user ID' => [
262                 'id', true, true
263             ],
264             'Fetch data using a non-existent user ID' => [
265                 'id', false, false
266             ],
267             'Fetch data using a valid username' => [
268                 'username', 's1', true
269             ],
270             'Fetch data using an invalid username' => [
271                 'username', 's2', false
272             ],
273             'Fetch data using a valid ID Number' => [
274                 'idnumber', 's1', true
275             ],
276             'Fetch data using an invalid ID Number' => [
277                 'idnumber', 's2', false
278             ],
279         ];
280     }
282     /**
283      * Check that the user matches a user in the system.
284      *
285      * @dataProvider check_user_exists_provider
286      * @param string $field The field to use for the query.
287      * @param string|boolean $value The field value. When fetching by ID, set true to fetch valid user ID, false otherwise.
288      * @param boolean $successexpected Whether we expect for a user to be found or not.
289      * @param int $allowaccountssameemail Value for $CFG->allowaccountssameemail
290      */
291     public function test_check_user_exists($field, $value, $successexpected, $allowaccountssameemail = 0) {
292         $this->resetAfterTest();
294         $generator = $this->getDataGenerator();
296         // Need to add one of the users into the system.
297         $user = $generator->create_user([
298             'firstname' => 'Anne',
299             'lastname' => 'Able',
300             'email' => 's1@example.com',
301             'idnumber' => 's1',
302             'username' => 's1',
303         ]);
305         if ($allowaccountssameemail) {
306             // Create another user with the same email address.
307             $generator->create_user(['email' => 's1@example.com']);
308         }
310         // Since the data provider can't know what user ID to use, do a special handling for ID field tests.
311         if ($field === 'id') {
312             if ($value) {
313                 // Test for fetching data using a valid user ID. Use the generated user's ID.
314                 $value = $user->id;
315             } else {
316                 // Test for fetching data using a non-existent user ID.
317                 $value = $user->id + 1;
318             }
319         }
321         $userfields = [
322             'field' => $field,
323             'label' => 'Field label: ' . $field
324         ];
326         $testobject = new phpunit_gradeimport_csv_load_data();
328         // Check whether the user exists. If so, then the user id is returned. Otherwise, it returns null.
329         $userid = $testobject->test_check_user_exists($value, $userfields);
331         if ($successexpected) {
332             // Check that the user id returned matches with the user that we created.
333             $this->assertEquals($user->id, $userid);
335             // Check that there are no errors.
336             $this->assertEmpty($testobject->get_gradebookerrors());
338         } else {
339             // Check that the userid is null.
340             $this->assertNull($userid);
342             // Check that expected error message and actual message match.
343             $gradebookerrors = $testobject->get_gradebookerrors();
344             $mappingobject = (object)[
345                 'field' => $userfields['label'],
346                 'value' => $value,
347             ];
348             if ($allowaccountssameemail) {
349                 $expectederrormessage = get_string('usermappingerrormultipleusersfound', 'grades', $mappingobject);
350             } else {
351                 $expectederrormessage = get_string('usermappingerror', 'grades', $mappingobject);
352             }
354             $this->assertEquals($expectederrormessage, $gradebookerrors[0]);
355         }
356     }
358     /**
359      * Test preparing feedback for inserting / updating into the gradebook.
360      */
361     public function test_create_feedback() {
363         $testarray = $this->csv_load($this->oktext);
364         $testobject = new phpunit_gradeimport_csv_load_data();
366         // Try to insert some feedback for an assessment.
367         $feedback = $testobject->test_create_feedback($this->courseid, 1, $testarray[0][7]);
369         // Expected result.
370         $expectedfeedback = array('itemid' => 1, 'feedback' => $testarray[0][7]);
371         $this->assertEquals((array)$feedback, $expectedfeedback);
372     }
374     /**
375      * Test preparing grade_items for upgrading into the gradebook.
376      */
377     public function test_update_grade_item() {
379         $testarray = $this->csv_load($this->oktext);
380         $testobject = new phpunit_gradeimport_csv_load_data();
382         // We're not using scales so no to this option.
383         $verbosescales = 0;
384         // Map and key are to retrieve the grade_item that we are updating.
385         $map = array(1);
386         $key = 0;
387         // We return the new grade array for saving.
388         $newgrades = $testobject->test_update_grade_item($this->courseid, $map, $key, $verbosescales, $testarray[0][6]);
390         $expectedresult = array();
391         $expectedresult[0] = new stdClass();
392         $expectedresult[0]->itemid = 1;
393         $expectedresult[0]->finalgrade = $testarray[0][6];
395         $this->assertEquals($newgrades, $expectedresult);
397         // Try sending a bad grade value (A letter instead of a float / int).
398         $newgrades = $testobject->test_update_grade_item($this->courseid, $map, $key, $verbosescales, 'A');
399         // The $newgrades variable should be null.
400         $this->assertNull($newgrades);
401         $expectederrormessage = get_string('badgrade', 'grades');
402         // Check that the error message is what we expect.
403         $gradebookerrors = $testobject->get_gradebookerrors();
404         $this->assertEquals($expectederrormessage, $gradebookerrors[0]);
405     }
407     /**
408      * Test importing data and mapping it with items in the course.
409      */
410     public function test_map_user_data_with_value() {
411         // Need to add one of the users into the system.
412         $user = new stdClass();
413         $user->firstname = 'Anne';
414         $user->lastname = 'Able';
415         $user->email = 'student7@example.com';
416         $userdetail = $this->getDataGenerator()->create_user($user);
418         $testarray = $this->csv_load($this->oktext);
419         $testobject = new phpunit_gradeimport_csv_load_data();
421         // We're not using scales so no to this option.
422         $verbosescales = 0;
423         // Map and key are to retrieve the grade_item that we are updating.
424         $map = array(1);
425         $key = 0;
427         // Test new user mapping. This should return the user id if there were no problems.
428         $userid = $testobject->test_map_user_data_with_value('useremail', $testarray[0][5], $this->columns, $map, $key,
429                 $this->courseid, $map[$key], $verbosescales);
430         $this->assertEquals($userid, $userdetail->id);
432         $newgrades = $testobject->test_map_user_data_with_value('new', $testarray[0][6], $this->columns, $map, $key,
433                 $this->courseid, $map[$key], $verbosescales);
434         // Check that the final grade is the same as the one inserted.
435         $this->assertEquals($testarray[0][6], $newgrades[0]->finalgrade);
437         $newgrades = $testobject->test_map_user_data_with_value('new', $testarray[0][8], $this->columns, $map, $key,
438                 $this->courseid, $map[$key], $verbosescales);
439         // Check that the final grade is the same as the one inserted.
440         // The testobject should now contain 2 new grade items.
441         $this->assertEquals(2, count($newgrades));
442         // Because this grade item is empty, the value for final grade should be null.
443         $this->assertNull($newgrades[1]->finalgrade);
445         $feedback = $testobject->test_map_user_data_with_value('feedback', $testarray[0][7], $this->columns, $map, $key,
446                 $this->courseid, $map[$key], $verbosescales);
447         // Expected result.
448         $resultarray = array();
449         $resultarray[0] = new stdClass();
450         $resultarray[0]->itemid = 1;
451         $resultarray[0]->feedback = $testarray[0][7];
452         $this->assertEquals($feedback, $resultarray);
454         // Default behaviour (update a grade item).
455         $newgrades = $testobject->test_map_user_data_with_value('default', $testarray[0][6], $this->columns, $map, $key,
456                 $this->courseid, $map[$key], $verbosescales);
457         $this->assertEquals($testarray[0][6], $newgrades[0]->finalgrade);
458     }
460     /**
461      * Test importing data into the gradebook.
462      */
463     public function test_prepare_import_grade_data() {
464         global $DB;
466         // Need to add one of the users into the system.
467         $user = new stdClass();
468         $user->firstname = 'Anne';
469         $user->lastname = 'Able';
470         $user->email = 'student7@example.com';
471         // Insert user 1.
472         $this->getDataGenerator()->create_user($user);
473         $user = new stdClass();
474         $user->firstname = 'Bobby';
475         $user->lastname = 'Bunce';
476         $user->email = 'student5@example.com';
477         // Insert user 2.
478         $this->getDataGenerator()->create_user($user);
480         $this->csv_load($this->oktext);
482         $importcode = 007;
483         $verbosescales = 0;
485         // Form data object.
486         $formdata = new stdClass();
487         $formdata->mapfrom = 5;
488         $formdata->mapto = 'useremail';
489         $formdata->mapping_0 = 0;
490         $formdata->mapping_1 = 0;
491         $formdata->mapping_2 = 0;
492         $formdata->mapping_3 = 0;
493         $formdata->mapping_4 = 0;
494         $formdata->mapping_5 = 0;
495         $formdata->mapping_6 = 'new';
496         $formdata->mapping_7 = 'feedback_2';
497         $formdata->mapping_8 = 0;
498         $formdata->mapping_9 = 0;
499         $formdata->map = 1;
500         $formdata->id = 2;
501         $formdata->iid = $this->iid;
502         $formdata->importcode = $importcode;
503         $formdata->forceimport = false;
505         // Blam go time.
506         $testobject = new phpunit_gradeimport_csv_load_data();
507         $dataloaded = $testobject->prepare_import_grade_data($this->columns, $formdata, $this->csvimport, $this->courseid, '', '',
508                 $verbosescales);
509         // If everything inserted properly then this should be true.
510         $this->assertTrue($dataloaded);
511     }
513     /*
514      * Test importing csv data into the gradebook using "Last downloaded from this course" column and force import option.
515      */
516     public function test_force_import_option () {
518         // Need to add users into the system.
519         $user = new stdClass();
520         $user->firstname = 'Anne';
521         $user->lastname = 'Able';
522         $user->email = 'student7@example.com';
523         $user->id_number = 1;
524         $user1 = $this->getDataGenerator()->create_user($user);
525         $user = new stdClass();
526         $user->firstname = 'Bobby';
527         $user->lastname = 'Bunce';
528         $user->email = 'student5@example.com';
529         $user->id_number = 2;
530         $user2 = $this->getDataGenerator()->create_user($user);
532         // Create a new grade item.
533         $params = array(
534             'itemtype'  => 'manual',
535             'itemname'  => 'Grade item 1',
536             'gradetype' => GRADE_TYPE_VALUE,
537             'courseid'  => $this->courseid
538         );
539         $gradeitem = new grade_item($params, false);
540         $gradeitemid = $gradeitem->insert();
542         $importcode = 001;
543         $verbosescales = 0;
545         // Form data object.
546         $formdata = new stdClass();
547         $formdata->mapfrom = 5;
548         $formdata->mapto = 'useremail';
549         $formdata->mapping_0 = 0;
550         $formdata->mapping_1 = 0;
551         $formdata->mapping_2 = 0;
552         $formdata->mapping_3 = 0;
553         $formdata->mapping_4 = 0;
554         $formdata->mapping_5 = 0;
555         $formdata->mapping_6 = $gradeitemid;
556         $formdata->mapping_7 = 'feedback_2';
557         $formdata->mapping_8 = 0;
558         $formdata->mapping_9 = 0;
559         $formdata->map = 1;
560         $formdata->id = 2;
561         $formdata->iid = $this->iid;
562         $formdata->importcode = $importcode;
563         $formdata->forceimport = false;
565         // Add last download from this course column to csv content.
566         $exportdate = time();
567         $newcsvdata = str_replace('{exportdate}', $exportdate, $this->csvtext);
568         $this->csv_load($newcsvdata);
569         $testobject = new phpunit_gradeimport_csv_load_data();
570         $dataloaded = $testobject->prepare_import_grade_data($this->columns, $formdata, $this->csvimport,
571                 $this->courseid, '', '', $verbosescales);
572         $this->assertTrue($dataloaded);
574         // We must update the last modified date.
575         grade_import_commit($this->courseid, $importcode, false, false);
577         // Test using force import disabled and a date in the past.
578         $pastdate = strtotime('-1 day', time());
579         $newcsvdata = str_replace('{exportdate}', $pastdate, $this->csvtext);
580         $this->csv_load($newcsvdata);
581         $testobject = new phpunit_gradeimport_csv_load_data();
582         $dataloaded = $testobject->prepare_import_grade_data($this->columns, $formdata, $this->csvimport,
583                 $this->courseid, '', '', $verbosescales);
584         $this->assertFalse($dataloaded);
585         $errors = $testobject->get_gradebookerrors();
586         $this->assertEquals($errors[0], get_string('gradealreadyupdated', 'grades', fullname($user1)));
588         // Test using force import enabled and a date in the past.
589         $formdata->forceimport = true;
590         $testobject = new phpunit_gradeimport_csv_load_data();
591         $dataloaded = $testobject->prepare_import_grade_data($this->columns, $formdata, $this->csvimport,
592                 $this->courseid, '', '', $verbosescales);
593         $this->assertTrue($dataloaded);
595         // Test importing using an old exported file (2 years ago).
596         $formdata->forceimport = false;
597         $twoyearsago = strtotime('-2 year', time());
598         $newcsvdata = str_replace('{exportdate}', $twoyearsago, $this->csvtext);
599         $this->csv_load($newcsvdata);
600         $testobject = new phpunit_gradeimport_csv_load_data();
601         $dataloaded = $testobject->prepare_import_grade_data($this->columns, $formdata, $this->csvimport,
602                 $this->courseid, '', '', $verbosescales);
603         $this->assertFalse($dataloaded);
604         $errors = $testobject->get_gradebookerrors();
605         $this->assertEquals($errors[0], get_string('invalidgradeexporteddate', 'grades'));
607         // Test importing using invalid exported date.
608         $baddate = '0123A56B89';
609         $newcsvdata = str_replace('{exportdate}', $baddate, $this->csvtext);
610         $this->csv_load($newcsvdata);
611         $formdata->mapping_6 = $gradeitemid;
612         $testobject = new phpunit_gradeimport_csv_load_data();
613         $dataloaded = $testobject->prepare_import_grade_data($this->columns, $formdata, $this->csvimport,
614                 $this->courseid, '', '', $verbosescales);
615         $this->assertFalse($dataloaded);
616         $errors = $testobject->get_gradebookerrors();
617         $this->assertEquals($errors[0], get_string('invalidgradeexporteddate', 'grades'));
619         // Test importing using date in the future.
620         $oneyearahead = strtotime('+1 year', time());
621         $oldcsv = str_replace('{exportdate}', $oneyearahead, $this->csvtext);
622         $this->csv_load($oldcsv);
623         $formdata->mapping_6 = $gradeitemid;
624         $testobject = new phpunit_gradeimport_csv_load_data();
625         $dataloaded = $testobject->prepare_import_grade_data($this->columns, $formdata, $this->csvimport,
626             $this->courseid, '', '', $verbosescales);
627         $this->assertFalse($dataloaded);
628         $errors = $testobject->get_gradebookerrors();
629         $this->assertEquals($errors[0], get_string('invalidgradeexporteddate', 'grades'));
630     }