1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Tests for step.
19 *
20 * @package    tool_usertours
21 * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
22 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25defined('MOODLE_INTERNAL') || die();
26
27global $CFG;
28require_once($CFG->libdir . '/formslib.php');
29
30/**
31 * Tests for step.
32 *
33 * @package    tool_usertours
34 * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
35 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36 */
37class step_testcase extends advanced_testcase {
38
39    /**
40     * @var moodle_database
41     */
42    protected $db;
43
44    /**
45     * Setup to store the DB reference.
46     */
47    public function setUp(): void {
48        global $DB;
49
50        $this->db = $DB;
51    }
52
53    /**
54     * Tear down to restore the original DB reference.
55     */
56    public function tearDown(): void {
57        global $DB;
58
59        $DB = $this->db;
60    }
61
62    /**
63     * Helper to mock the database.
64     *
65     * @return moodle_database
66     */
67    public function mock_database() {
68        global $DB;
69
70        $DB = $this->getMockBuilder('moodle_database')
71            ->getMock()
72            ;
73
74        return $DB;
75    }
76
77    /**
78     * Data provider for the dirty value tester.
79     *
80     * @return array
81     */
82    public function dirty_value_provider() {
83        return [
84                'tourid' => [
85                        'tourid',
86                        [1],
87                    ],
88                'title' => [
89                        'title',
90                        ['Lorem'],
91                    ],
92                'content' => [
93                        'content',
94                        ['Lorem'],
95                    ],
96                'targettype' => [
97                        'targettype',
98                        ['Lorem'],
99                    ],
100                'targetvalue' => [
101                        'targetvalue',
102                        ['Lorem'],
103                    ],
104                'sortorder' => [
105                        'sortorder',
106                        [1],
107                    ],
108                'config' => [
109                        'config',
110                        ['key', 'value'],
111                    ],
112            ];
113    }
114
115    /**
116     * Test the fetch function.
117     */
118    public function test_fetch() {
119        $step = $this->getMockBuilder(\tool_usertours\step::class)
120            ->setMethods(['reload_from_record'])
121            ->getMock()
122            ;
123
124        $idretval = rand(1, 100);
125        $DB = $this->mock_database();
126        $DB->method('get_record')
127            ->willReturn($idretval)
128            ;
129
130        $retval = rand(1, 100);
131        $step->expects($this->once())
132            ->method('reload_from_record')
133            ->with($this->equalTo($idretval))
134            ->wilLReturn($retval)
135            ;
136
137        $rc = new \ReflectionClass(\tool_usertours\step::class);
138        $rcm = $rc->getMethod('fetch');
139        $rcm->setAccessible(true);
140
141        $id = rand(1, 100);
142        $this->assertEquals($retval, $rcm->invoke($step, 'fetch', $id));
143    }
144
145    /**
146     * Test that setters mark things as dirty.
147     *
148     * @dataProvider dirty_value_provider
149     * @param   string  $name       The key to update
150     * @param   string  $value      The value to set
151     */
152    public function test_dirty_values($name, $value) {
153        $step = new \tool_usertours\step();
154        $method = 'set_' . $name;
155        call_user_func_array([$step, $method], $value);
156
157        $rc = new \ReflectionClass(\tool_usertours\step::class);
158        $rcp = $rc->getProperty('dirty');
159        $rcp->setAccessible(true);
160
161        $this->assertTrue($rcp->getValue($step));
162    }
163
164    /**
165     * Provider for is_first_step.
166     *
167     * @return array
168     */
169    public function step_sortorder_provider() {
170        return [
171                [0, 5, true, false],
172                [1, 5, false, false],
173                [4, 5, false, true],
174            ];
175    }
176
177    /**
178     * Test is_first_step.
179     *
180     * @dataProvider step_sortorder_provider
181     * @param   int     $sortorder      The sortorder to check
182     * @param   int     $count          Unused in this function
183     * @param   bool    $isfirst        Whether this is the first step
184     * @param   bool    $islast         Whether this is the last step
185     */
186    public function test_is_first_step($sortorder, $count, $isfirst, $islast) {
187        $step = $this->getMockBuilder(\tool_usertours\step::class)
188            ->setMethods(['get_sortorder'])
189            ->getMock();
190
191        $step->expects($this->once())
192            ->method('get_sortorder')
193            ->willReturn($sortorder)
194            ;
195
196        $this->assertEquals($isfirst, $step->is_first_step());
197    }
198
199    /**
200     * Test is_last_step.
201     *
202     * @dataProvider step_sortorder_provider
203     * @param   int     $sortorder      The sortorder to check
204     * @param   int     $count          Total number of steps for this test
205     * @param   bool    $isfirst        Whether this is the first step
206     * @param   bool    $islast         Whether this is the last step
207     */
208    public function test_is_last_step($sortorder, $count, $isfirst, $islast) {
209        $step = $this->getMockBuilder(\tool_usertours\step::class)
210            ->setMethods(['get_sortorder', 'get_tour'])
211            ->getMock();
212
213        $tour = $this->getMockBuilder(\tool_usertours\tour::class)
214            ->setMethods(['count_steps'])
215            ->getMock();
216
217        $step->expects($this->once())
218            ->method('get_tour')
219            ->willReturn($tour)
220            ;
221
222        $tour->expects($this->once())
223            ->method('count_steps')
224            ->willReturn($count)
225            ;
226
227        $step->expects($this->once())
228            ->method('get_sortorder')
229            ->willReturn($sortorder)
230            ;
231
232        $this->assertEquals($islast, $step->is_last_step());
233    }
234
235    /**
236     * Test get_config with no keys provided.
237     */
238    public function test_get_config_no_keys() {
239        $step = new \tool_usertours\step();
240
241        $rc = new \ReflectionClass(\tool_usertours\step::class);
242        $rcp = $rc->getProperty('config');
243        $rcp->setAccessible(true);
244
245        $allvalues = (object) [
246                'some' => 'value',
247                'another' => 42,
248                'key' => [
249                    'somethingelse',
250                ],
251            ];
252
253        $rcp->setValue($step, $allvalues);
254
255        $this->assertEquals($allvalues, $step->get_config());
256    }
257
258    /**
259     * Data provider for get_config.
260     *
261     * @return array
262     */
263    public function get_config_provider() {
264        $allvalues = (object) [
265                'some' => 'value',
266                'another' => 42,
267                'key' => [
268                    'somethingelse',
269                ],
270            ];
271
272        $tourconfig = rand(1, 100);
273        $forcedconfig = rand(1, 100);
274
275        return [
276                'No initial config' => [
277                        null,
278                        null,
279                        null,
280                        $tourconfig,
281                        false,
282                        $forcedconfig,
283                        (object) [],
284                    ],
285                'All values' => [
286                        $allvalues,
287                        null,
288                        null,
289                        $tourconfig,
290                        false,
291                        $forcedconfig,
292                        $allvalues,
293                    ],
294                'Valid string value' => [
295                        $allvalues,
296                        'some',
297                        null,
298                        $tourconfig,
299                        false,
300                        $forcedconfig,
301                        'value',
302                    ],
303                'Valid array value' => [
304                        $allvalues,
305                        'key',
306                        null,
307                        $tourconfig,
308                        false,
309                        $forcedconfig,
310                        ['somethingelse'],
311                    ],
312                'Invalid value' => [
313                        $allvalues,
314                        'notavalue',
315                        null,
316                        $tourconfig,
317                        false,
318                        $forcedconfig,
319                        $tourconfig,
320                    ],
321                'Configuration value' => [
322                        $allvalues,
323                        'placement',
324                        null,
325                        $tourconfig,
326                        false,
327                        $forcedconfig,
328                        $tourconfig,
329                    ],
330                'Invalid value with default' => [
331                        $allvalues,
332                        'notavalue',
333                        'somedefault',
334                        $tourconfig,
335                        false,
336                        $forcedconfig,
337                        'somedefault',
338                    ],
339                'Value forced at target' => [
340                        $allvalues,
341                        'somevalue',
342                        'somedefault',
343                        $tourconfig,
344                        true,
345                        $forcedconfig,
346                        $forcedconfig,
347                    ],
348            ];
349    }
350
351    /**
352     * Test get_config with valid keys provided.
353     *
354     * @dataProvider get_config_provider
355     * @param   object  $values     The config values
356     * @param   string  $key        The key
357     * @param   mixed   $default    The default value
358     * @param   mixed   $tourconfig The tour config
359     * @param   bool    $isforced   Whether the setting is forced
360     * @param   mixed   $forcedvalue    The example value
361     * @param   mixed   $expected   The expected value
362     */
363    public function test_get_config_valid_keys($values, $key, $default, $tourconfig, $isforced, $forcedvalue, $expected) {
364        $step = $this->getMockBuilder(\tool_usertours\step::class)
365            ->setMethods(['get_target', 'get_targettype', 'get_tour'])
366            ->getMock();
367
368        $rc = new \ReflectionClass(\tool_usertours\step::class);
369        $rcp = $rc->getProperty('config');
370        $rcp->setAccessible(true);
371        $rcp->setValue($step, $values);
372
373        $target = $this->getMockBuilder(\tool_usertours\local\target\base::class)
374            ->disableOriginalConstructor()
375            ->getMock()
376            ;
377
378        $target->expects($this->any())
379            ->method('is_setting_forced')
380            ->willReturn($isforced)
381            ;
382
383        $target->expects($this->any())
384            ->method('get_forced_setting_value')
385            ->with($this->equalTo($key))
386            ->willReturn($forcedvalue)
387            ;
388
389        $step->expects($this->any())
390            ->method('get_targettype')
391            ->willReturn('type')
392            ;
393
394        $step->expects($this->any())
395            ->method('get_target')
396            ->willReturn($target)
397            ;
398
399        $tour = $this->getMockBuilder(\tool_usertours\tour::class)
400            ->getMock()
401            ;
402
403        $tour->expects($this->any())
404            ->method('get_config')
405            ->willReturn($tourconfig)
406            ;
407
408        $step->expects($this->any())
409            ->method('get_tour')
410            ->willReturn($tour)
411            ;
412
413        $this->assertEquals($expected, $step->get_config($key, $default));
414    }
415
416    /**
417     * Data provider for set_config.
418     */
419    public function set_config_provider() {
420        $allvalues = (object) [
421                'some' => 'value',
422                'another' => 42,
423                'key' => [
424                    'somethingelse',
425                ],
426            ];
427
428        $randvalue = rand(1, 100);
429
430        $provider = [];
431
432        $newvalues = $allvalues;
433        $newvalues->some = 'unset';
434        $provider['Unset an existing value'] = [
435                $allvalues,
436                'some',
437                null,
438                $newvalues,
439            ];
440
441        $newvalues = $allvalues;
442        $newvalues->some = $randvalue;
443        $provider['Set an existing value'] = [
444                $allvalues,
445                'some',
446                $randvalue,
447                $newvalues,
448            ];
449
450        $provider['Set a new value'] = [
451                $allvalues,
452                'newkey',
453                $randvalue,
454                (object) array_merge((array) $allvalues, ['newkey' => $randvalue]),
455            ];
456
457        return $provider;
458    }
459
460    /**
461     * Test that set_config works in the anticipated fashion.
462     *
463     * @dataProvider set_config_provider
464     * @param   mixed   $initialvalues  The inital value to set
465     * @param   string  $key        The key to test
466     * @param   mixed   $newvalue   The new value to set
467     * @param   mixed   $expected   The expected value
468     */
469    public function test_set_config($initialvalues, $key, $newvalue, $expected) {
470        $step = new \tool_usertours\step();
471
472        $rc = new \ReflectionClass(\tool_usertours\step::class);
473        $rcp = $rc->getProperty('config');
474        $rcp->setAccessible(true);
475        $rcp->setValue($step, $initialvalues);
476
477        $target = $this->getMockBuilder(\tool_usertours\local\target\base::class)
478            ->disableOriginalConstructor()
479            ->getMock()
480            ;
481
482        $target->expects($this->any())
483            ->method('is_setting_forced')
484            ->willReturn(false)
485            ;
486
487        $step->set_config($key, $newvalue);
488
489        $this->assertEquals($expected, $rcp->getValue($step));
490    }
491
492    /**
493     * Ensure that non-dirty tours are not persisted.
494     */
495    public function test_persist_non_dirty() {
496        $step = $this->getMockBuilder(\tool_usertours\step::class)
497            ->setMethods([
498                    'to_record',
499                    'reload',
500                ])
501            ->getMock()
502            ;
503
504        $step->expects($this->never())
505            ->method('to_record')
506            ;
507
508        $step->expects($this->never())
509            ->method('reload')
510            ;
511
512        $this->assertSame($step, $step->persist());
513    }
514
515    /**
516     * Ensure that new dirty steps are persisted.
517     */
518    public function test_persist_dirty_new() {
519        // Mock the database.
520        $DB = $this->mock_database();
521        $DB->expects($this->once())
522            ->method('insert_record')
523            ->willReturn(42)
524            ;
525
526        // Mock the tour.
527        $step = $this->getMockBuilder(\tool_usertours\step::class)
528            ->setMethods([
529                    'to_record',
530                    'calculate_sortorder',
531                    'reload',
532                ])
533            ->getMock()
534            ;
535
536        $step->expects($this->once())
537            ->method('to_record')
538            ->willReturn((object)['id' => 42]);
539            ;
540
541        $step->expects($this->once())
542            ->method('calculate_sortorder')
543            ;
544
545        $step->expects($this->once())
546            ->method('reload')
547            ;
548
549        $rc = new \ReflectionClass(\tool_usertours\step::class);
550        $rcp = $rc->getProperty('dirty');
551        $rcp->setAccessible(true);
552        $rcp->setValue($step, true);
553
554        $tour = $this->createMock(\tool_usertours\tour::class);
555        $rcp = $rc->getProperty('tour');
556        $rcp->setAccessible(true);
557        $rcp->setValue($step, $tour);
558
559        $this->assertSame($step, $step->persist());
560    }
561
562    /**
563     * Ensure that new non-dirty, forced steps are persisted.
564     */
565    public function test_persist_force_new() {
566        global $DB;
567
568        // Mock the database.
569        $DB = $this->mock_database();
570        $DB->expects($this->once())
571            ->method('insert_record')
572            ->willReturn(42)
573            ;
574
575        // Mock the tour.
576        $step = $this->getMockBuilder(\tool_usertours\step::class)
577            ->setMethods([
578                    'to_record',
579                    'calculate_sortorder',
580                    'reload',
581                ])
582            ->getMock()
583            ;
584
585        $step->expects($this->once())
586            ->method('to_record')
587            ->willReturn((object)['id' => 42]);
588            ;
589
590        $step->expects($this->once())
591            ->method('calculate_sortorder')
592            ;
593
594        $step->expects($this->once())
595            ->method('reload')
596            ;
597
598        $tour = $this->createMock(\tool_usertours\tour::class);
599        $rc = new \ReflectionClass(\tool_usertours\step::class);
600        $rcp = $rc->getProperty('tour');
601        $rcp->setAccessible(true);
602        $rcp->setValue($step, $tour);
603
604        $this->assertSame($step, $step->persist(true));
605    }
606
607    /**
608     * Ensure that existing dirty steps are persisted.
609     */
610    public function test_persist_dirty_existing() {
611        // Mock the database.
612        $DB = $this->mock_database();
613        $DB->expects($this->once())
614            ->method('update_record')
615            ;
616
617        // Mock the tour.
618        $step = $this->getMockBuilder(\tool_usertours\step::class)
619            ->setMethods([
620                    'to_record',
621                    'calculate_sortorder',
622                    'reload',
623                ])
624            ->getMock()
625            ;
626
627        $step->expects($this->once())
628            ->method('to_record')
629            ->willReturn((object)['id' => 42]);
630            ;
631
632        $step->expects($this->never())
633            ->method('calculate_sortorder')
634            ;
635
636        $step->expects($this->once())
637            ->method('reload')
638            ;
639
640        $rc = new \ReflectionClass(\tool_usertours\step::class);
641        $rcp = $rc->getProperty('id');
642        $rcp->setAccessible(true);
643        $rcp->setValue($step, 42);
644
645        $rcp = $rc->getProperty('dirty');
646        $rcp->setAccessible(true);
647        $rcp->setValue($step, true);
648
649        $tour = $this->createMock(\tool_usertours\tour::class);
650        $rcp = $rc->getProperty('tour');
651        $rcp->setAccessible(true);
652        $rcp->setValue($step, $tour);
653
654        $this->assertSame($step, $step->persist());
655    }
656
657    /**
658     * Ensure that existing non-dirty, forced steps are persisted.
659     */
660    public function test_persist_force_existing() {
661        global $DB;
662
663        // Mock the database.
664        $DB = $this->mock_database();
665        $DB->expects($this->once())
666            ->method('update_record')
667            ;
668
669        // Mock the tour.
670        $step = $this->getMockBuilder(\tool_usertours\step::class)
671            ->setMethods([
672                    'to_record',
673                    'calculate_sortorder',
674                    'reload',
675                ])
676            ->getMock()
677            ;
678
679        $step->expects($this->once())
680            ->method('to_record')
681            ->willReturn((object)['id' => 42]);
682            ;
683
684        $step->expects($this->never())
685            ->method('calculate_sortorder')
686            ;
687
688        $step->expects($this->once())
689            ->method('reload')
690            ;
691
692        $rc = new \ReflectionClass(\tool_usertours\step::class);
693        $rcp = $rc->getProperty('id');
694        $rcp->setAccessible(true);
695        $rcp->setValue($step, 42);
696
697        $tour = $this->createMock(\tool_usertours\tour::class);
698        $rcp = $rc->getProperty('tour');
699        $rcp->setAccessible(true);
700        $rcp->setValue($step, $tour);
701
702        $this->assertSame($step, $step->persist(true));
703    }
704
705    /**
706     * Check that a tour which has never been persisted is removed correctly.
707     */
708    public function test_remove_non_persisted() {
709        $step = $this->getMockBuilder(\tool_usertours\step::class)
710            ->setMethods(null)
711            ->getMock()
712            ;
713
714        // Mock the database.
715        $DB = $this->mock_database();
716        $DB->expects($this->never())
717            ->method('delete_records')
718            ;
719
720        $this->assertNull($step->remove());
721    }
722
723    /**
724     * Check that a tour which has been persisted is removed correctly.
725     */
726    public function test_remove_persisted() {
727        $id = rand(1, 100);
728
729        $tour = $this->getMockBuilder(\tool_usertours\tour::class)
730            ->setMethods([
731                    'reset_step_sortorder',
732                ])
733            ->getMock()
734            ;
735
736        $tour->expects($this->once())
737            ->method('reset_step_sortorder')
738            ;
739
740        $step = $this->getMockBuilder(\tool_usertours\step::class)
741            ->setMethods([
742                    'get_tour',
743                ])
744            ->getMock()
745            ;
746
747        $step->expects($this->once())
748            ->method('get_tour')
749            ->willReturn($tour)
750            ;
751
752        // Mock the database.
753        $DB = $this->mock_database();
754        $DB->expects($this->once())
755            ->method('delete_records')
756            ->with($this->equalTo('tool_usertours_steps'), $this->equalTo(['id' => $id]))
757            ;
758
759        $rc = new \ReflectionClass(\tool_usertours\step::class);
760        $rcp = $rc->getProperty('id');
761        $rcp->setAccessible(true);
762        $rcp->setValue($step, $id);
763
764        $this->assertEquals($id, $step->get_id());
765        $this->assertNull($step->remove());
766    }
767
768    /**
769     * Data provider for the get_ tests.
770     *
771     * @return array
772     */
773    public function getter_provider() {
774        return [
775                'id' => [
776                        'id',
777                        rand(1, 100),
778                    ],
779                'tourid' => [
780                        'tourid',
781                        rand(1, 100),
782                    ],
783                'title' => [
784                        'title',
785                        'Lorem',
786                    ],
787                'content' => [
788                        'content',
789                        'Lorem',
790                    ],
791                'targettype' => [
792                        'targettype',
793                        'Lorem',
794                    ],
795                'targetvalue' => [
796                        'targetvalue',
797                        'Lorem',
798                    ],
799                'sortorder' => [
800                        'sortorder',
801                        rand(1, 100),
802                    ],
803            ];
804    }
805
806    /**
807     * Test that getters return the configured value.
808     *
809     * @dataProvider getter_provider
810     * @param   string  $key        The key to test
811     * @param   mixed   $value      The expected value
812     */
813    public function test_getters($key, $value) {
814        $step = new \tool_usertours\step();
815
816        $rc = new \ReflectionClass(\tool_usertours\step::class);
817
818        $rcp = $rc->getProperty($key);
819        $rcp->setAccessible(true);
820        $rcp->setValue($step, $value);
821
822        $getter = 'get_' . $key;
823
824        $this->assertEquals($value, $step->$getter());
825    }
826
827    /**
828     * Data Provider for get_string_from_input.
829     *
830     * @return array
831     */
832    public function get_string_from_input_provider() {
833        return [
834            'Text'  => [
835                'example',
836                'example',
837            ],
838            'Text which looks like a langstring' => [
839                'example,fakecomponent',
840                'example,fakecomponent',
841            ],
842            'Text which is a langstring' => [
843                'administration,core',
844                'Administration',
845            ],
846            'Text which is a langstring but uses "moodle" instead of "core"' => [
847                'administration,moodle',
848                'Administration',
849            ],
850            'Text which is a langstring, but with extra whitespace' => [
851                '  administration,moodle  ',
852                'Administration',
853            ],
854            'Looks like a langstring, but has incorrect space around comma' => [
855                'administration , moodle',
856                'administration , moodle',
857            ],
858        ];
859    }
860
861    /**
862     * Ensure that the get_string_from_input function returns langstring strings correctly.
863     *
864     * @dataProvider get_string_from_input_provider
865     * @param   string  $string     The string to test
866     * @param   string  $expected   The expected result
867     */
868    public function test_get_string_from_input($string, $expected) {
869        $this->assertEquals($expected, \tool_usertours\step::get_string_from_input($string));
870    }
871}
872