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 * Unit tests for privacy.
19 *
20 * @package   search_simpledb
21 * @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
22 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25use \search_simpledb\privacy\provider;
26use core_privacy\local\request\transform;
27use core_privacy\local\request\writer;
28
29defined('MOODLE_INTERNAL') || die();
30
31global $CFG;
32require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
33require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php');
34
35/**
36 * Unit tests for privacy.
37 *
38 * @package   search_simpledb
39 * @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
40 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41 */
42class privacy_model_testcase extends \core_privacy\tests\provider_testcase {
43
44    public function setUp(): void {
45        global $DB;
46
47        if ($this->requires_manual_index_update()) {
48            // We need to update fulltext index manually, which requires an alter table statement.
49            $this->preventResetByRollback();
50        }
51
52        $this->resetAfterTest();
53        set_config('enableglobalsearch', true);
54
55        // Inject search_simpledb engine into the testable core search as we need to add the mock
56        // search component to it.
57
58        $this->engine = new \search_simpledb\engine();
59        $this->search = testable_core_search::instance($this->engine);
60        $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
61        $this->search->add_search_area($areaid, new core_mocksearch\search\mock_search_area());
62
63        $this->generator = self::getDataGenerator()->get_plugin_generator('core_search');
64        $this->generator->setup();
65
66        $this->c1 = $this->getDataGenerator()->create_course();
67        $this->c2 = $this->getDataGenerator()->create_course();
68
69        $this->c1context = \context_course::instance($this->c1->id);
70        $this->c2context = \context_course::instance($this->c2->id);
71
72        $this->u1 = $this->getDataGenerator()->create_user();
73        $this->u2 = $this->getDataGenerator()->create_user();
74
75        $this->getDataGenerator()->enrol_user($this->u1->id, $this->c1->id, 'student');
76        $this->getDataGenerator()->enrol_user($this->u1->id, $this->c2->id, 'student');
77        $this->getDataGenerator()->enrol_user($this->u2->id, $this->c1->id, 'student');
78        $this->getDataGenerator()->enrol_user($this->u2->id, $this->c2->id, 'student');
79
80        $record = (object)[
81            'userid' => $this->u1->id,
82            'contextid' => $this->c1context->id,
83            'title' => 'vi',
84            'content' => 'va',
85            'description1' => 'san',
86            'description2' => 'jose'
87        ];
88        $this->generator->create_record($record);
89        $this->generator->create_record((object)['userid' => $this->u1->id, 'contextid' => $this->c2context->id]);
90        $this->generator->create_record((object)['userid' => $this->u2->id, 'contextid' => $this->c2context->id]);
91        $this->generator->create_record((object)['userid' => $this->u2->id, 'contextid' => $this->c1context->id]);
92        $this->generator->create_record((object)['owneruserid' => $this->u1->id, 'contextid' => $this->c1context->id]);
93        $this->generator->create_record((object)['owneruserid' => $this->u1->id, 'contextid' => $this->c2context->id]);
94        $this->generator->create_record((object)['owneruserid' => $this->u2->id, 'contextid' => $this->c1context->id]);
95        $this->generator->create_record((object)['owneruserid' => $this->u2->id, 'contextid' => $this->c2context->id]);
96        $this->search->index();
97
98        $this->setAdminUser();
99    }
100
101    /**
102     * tearDown
103     *
104     * @return void
105     */
106    public function tearDown(): void {
107        // Call parent tearDown() first.
108        parent::tearDown();
109
110        // For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup.
111        if ($this->generator) {
112            // Moodle DML freaks out if we don't teardown the temp table after each run.
113            $this->generator->teardown();
114            $this->generator = null;
115        }
116    }
117
118    /**
119     * Test fetching contexts for a given user ID.
120     */
121    public function test_get_contexts_for_userid() {
122        // Ensure both contexts are found for both users.
123        $expected = [$this->c1context->id, $this->c2context->id];
124        sort($expected);
125
126        // User 1.
127        $contextlist = provider::get_contexts_for_userid($this->u1->id);
128        $this->assertCount(2, $contextlist);
129
130        $actual = $contextlist->get_contextids();
131        sort($actual);
132        $this->assertEquals($expected, $actual);
133
134        // User 2.
135        $contextlist = provider::get_contexts_for_userid($this->u2->id);
136        $this->assertCount(2, $contextlist);
137
138        $actual = $contextlist->get_contextids();
139        sort($actual);
140        $this->assertEquals($expected, $actual);
141    }
142
143    /**
144     * Test fetching user IDs for a given context.
145     */
146    public function test_get_users_in_context() {
147        $component = 'search_simpledb';
148
149        // Ensure both users are found for both contexts.
150        $expected = [$this->u1->id, $this->u2->id];
151        sort($expected);
152
153        // User 1.
154        $userlist = new \core_privacy\local\request\userlist($this->c1context, $component);
155        provider::get_users_in_context($userlist);
156        $this->assertCount(2, $userlist);
157
158        $actual = $userlist->get_userids();
159        sort($actual);
160        $this->assertEquals($expected, $actual);
161
162        // User 2.
163        $userlist = new \core_privacy\local\request\userlist($this->c2context, $component);
164        provider::get_users_in_context($userlist);
165        $this->assertCount(2, $userlist);
166
167        $actual = $userlist->get_userids();
168        sort($actual);
169        $this->assertEquals($expected, $actual);
170    }
171
172    /**
173     * Test export user data.
174     *
175     * @return null
176     */
177    public function test_export_user_data() {
178        global $DB;
179
180        $contextlist = new \core_privacy\local\request\approved_contextlist($this->u1, 'search_simpledb',
181                                                                            [$this->c1context->id]);
182        provider::export_user_data($contextlist);
183        $writer = \core_privacy\local\request\writer::with_context($this->c1context);
184        $this->assertTrue($writer->has_any_data());
185        $u1c1 = $DB->get_record('search_simpledb_index', ['userid' => $this->u1->id, 'contextid' => $this->c1context->id]);
186        $data = $writer->get_data([get_string('search', 'search'), $u1c1->docid]);
187
188        $this->assertEquals($this->c1context->get_context_name(true, true), $data->context);
189        $this->assertEquals('vi', $data->title);
190        $this->assertEquals('va', $data->content);
191        $this->assertEquals('san', $data->description1);
192        $this->assertEquals('jose', $data->description2);
193    }
194
195    /**
196     * Test delete search for context.
197     *
198     * @return null
199     */
200    public function test_delete_data_for_all_users() {
201        global $DB;
202
203        $this->assertEquals(8, $DB->count_records('search_simpledb_index'));
204
205        provider::delete_data_for_all_users_in_context($this->c1context);
206        $this->assertEquals(0, $DB->count_records('search_simpledb_index', ['contextid' => $this->c1context->id]));
207        $this->assertEquals(4, $DB->count_records('search_simpledb_index'));
208
209        $u2context = \context_user::instance($this->u2->id);
210        provider::delete_data_for_all_users_in_context($u2context);
211        $this->assertEquals(0, $DB->count_records('search_simpledb_index', ['contextid' => $u2context->id]));
212        $this->assertEquals(2, $DB->count_records('search_simpledb_index'));
213    }
214
215    /**
216     * Test delete search for user.
217     *
218     * @return null
219     */
220    public function test_delete_data_for_user() {
221        global $DB;
222
223        $contextlist = new \core_privacy\local\request\approved_contextlist($this->u1, 'search_simpledb',
224                                                                            [$this->c1context->id]);
225        provider::delete_data_for_user($contextlist);
226        $select = 'contextid = :contextid AND (owneruserid = :owneruserid OR userid = :userid)';
227        $params = ['contextid' => $this->c1context->id, 'owneruserid' => $this->u1->id, 'userid' => $this->u1->id];
228        $this->assertEquals(0, $DB->count_records_select('search_simpledb_index', $select, $params));
229        $this->assertEquals(2, $DB->count_records('search_simpledb_index', ['contextid' => $this->c1context->id]));
230        $this->assertEquals(6, $DB->count_records('search_simpledb_index'));
231
232        $contextlist = new \core_privacy\local\request\approved_contextlist($this->u2, 'search_simpledb',
233                                                                            [$this->c2context->id]);
234        provider::delete_data_for_user($contextlist);
235        $select = 'contextid = :contextid AND (owneruserid = :owneruserid OR userid = :userid)';
236        $params = ['contextid' => $this->c2context->id, 'owneruserid' => $this->u2->id, 'userid' => $this->u2->id];
237        $this->assertEquals(0, $DB->count_records_select('search_simpledb_index', $select, $params));
238        $this->assertEquals(2, $DB->count_records('search_simpledb_index', ['contextid' => $this->c2context->id]));
239        $this->assertEquals(4, $DB->count_records('search_simpledb_index'));
240    }
241
242    /**
243     * Test deleting data for an approved userlist.
244     */
245    public function test_delete_data_for_users() {
246        global $DB;
247        $component = 'search_simpledb';
248        $select = 'contextid = :contextid AND (owneruserid = :owneruserid OR userid = :userid)';
249
250        // Ensure expected amount of data for both users exists in each context.
251        $this->assertEquals(4, $DB->count_records('search_simpledb_index', ['contextid' => $this->c1context->id]));
252        $this->assertEquals(4, $DB->count_records('search_simpledb_index', ['contextid' => $this->c2context->id]));
253
254        // Delete user 1's data in context 1.
255        $approveduserids = [$this->u1->id];
256        $approvedlist = new \core_privacy\local\request\approved_userlist($this->c1context, $component, $approveduserids);
257        provider::delete_data_for_users($approvedlist);
258
259        $params = ['contextid' => $this->c1context->id, 'owneruserid' => $this->u1->id, 'userid' => $this->u1->id];
260        $this->assertEquals(0, $DB->count_records_select('search_simpledb_index', $select, $params));
261
262        // Ensure user 2's data in context 1 is retained.
263        $params = ['contextid' => $this->c1context->id, 'owneruserid' => $this->u2->id, 'userid' => $this->u2->id];
264        $this->assertEquals(2, $DB->count_records_select('search_simpledb_index', $select, $params));
265
266        // Ensure both users' data in context 2 is retained.
267        $params = ['contextid' => $this->c2context->id, 'owneruserid' => $this->u1->id, 'userid' => $this->u1->id];
268        $this->assertEquals(2, $DB->count_records_select('search_simpledb_index', $select, $params));
269        $params = ['contextid' => $this->c2context->id, 'owneruserid' => $this->u2->id, 'userid' => $this->u2->id];
270        $this->assertEquals(2, $DB->count_records_select('search_simpledb_index', $select, $params));
271    }
272
273    /**
274     * Mssql with fulltext support requires manual updates.
275     *
276     * @return bool
277     */
278    private function requires_manual_index_update() {
279        global $DB;
280        return ($DB->get_dbfamily() === 'mssql' && $DB->is_fulltext_search_supported());
281    }
282}
283