2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Tests for the forum implementation of the Privacy Provider API.
21 * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die();
29 require_once(__DIR__ . '/helper.php');
30 require_once($CFG->dirroot . '/rating/lib.php');
32 use \mod_forum\privacy\provider;
35 * Tests for the forum implementation of the Privacy Provider API.
37 * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 class mod_forum_privacy_provider_testcase extends \core_privacy\tests\provider_testcase {
42 // Include the privacy subcontext_info trait.
43 // This includes the subcontext builders.
44 use \mod_forum\privacy\subcontext_info;
46 // Include the mod_forum test helpers.
47 // This includes functions to create forums, users, discussions, and posts.
50 // Include the privacy helper trait for the ratings API.
51 use \core_rating\phpunit\privacy_helper;
53 // Include the privacy helper trait for the tag API.
54 use \core_tag\tests\privacy_helper;
59 public function setUp() {
60 $this->resetAfterTest(true);
64 * Helper to assert that the forum data is correct.
66 * @param object $expected The expected data in the forum.
67 * @param object $actual The actual data in the forum.
69 protected function assert_forum_data($expected, $actual) {
71 $this->assertEquals(format_string($expected->name, true), $actual->name);
75 * Helper to assert that the discussion data is correct.
77 * @param object $expected The expected data in the discussion.
78 * @param object $actual The actual data in the discussion.
80 protected function assert_discussion_data($expected, $actual) {
82 $this->assertEquals(format_string($expected->name, true), $actual->name);
84 \core_privacy\local\request\transform::yesno($expected->pinned),
89 \core_privacy\local\request\transform::datetime($expected->timemodified),
94 \core_privacy\local\request\transform::datetime($expected->usermodified),
100 * Helper to assert that the post data is correct.
102 * @param object $expected The expected data in the post.
103 * @param object $actual The actual data in the post.
104 * @param \core_privacy\local\request\writer $writer The writer used
106 protected function assert_post_data($expected, $actual, $writer) {
108 $this->assertEquals(format_string($expected->subject, true), $actual->subject);
110 // The message should have been passed through the rewriter.
111 // Note: The testable rewrite_pluginfile_urls function in the ignores all items except the text.
113 $writer->rewrite_pluginfile_urls([], '', '', '', $expected->message),
118 \core_privacy\local\request\transform::datetime($expected->created),
123 \core_privacy\local\request\transform::datetime($expected->modified),
129 * Test that a user who is enrolled in a course, but who has never
130 * posted and has no other metadata stored will not have any link to
133 public function test_user_has_never_posted() {
134 // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.
135 $course = $this->getDataGenerator()->create_course();
136 $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
137 $course = $this->getDataGenerator()->create_course();
138 $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
139 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
140 list($user, $otheruser) = $this->helper_create_users($course, 2);
141 list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
142 $cm = get_coursemodule_from_instance('forum', $forum->id);
143 $context = \context_module::instance($cm->id);
145 // Test that no contexts were retrieved.
146 $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
147 $contexts = $contextlist->get_contextids();
148 $this->assertCount(0, $contexts);
150 // Attempting to export data for this context should return nothing either.
151 $this->export_context_data_for_user($user->id, $context, 'mod_forum');
153 $writer = \core_privacy\local\request\writer::with_context($context);
155 // The provider should always export data for any context explicitly asked of it, but there should be no
156 // metadata, files, or discussions.
157 $this->assertEmpty($writer->get_data([get_string('discussions', 'mod_forum')]));
158 $this->assertEmpty($writer->get_all_metadata([]));
159 $this->assertEmpty($writer->get_files([]));
163 * Test that a user who is enrolled in a course, and who has never
164 * posted and has subscribed to the forum will have relevant
165 * information returned.
167 public function test_user_has_never_posted_subscribed_to_forum() {
168 // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.
169 $course = $this->getDataGenerator()->create_course();
170 $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
171 $course = $this->getDataGenerator()->create_course();
172 $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
173 $course = $this->getDataGenerator()->create_course();
174 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
175 list($user, $otheruser) = $this->helper_create_users($course, 2);
176 list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
177 $cm = get_coursemodule_from_instance('forum', $forum->id);
178 $context = \context_module::instance($cm->id);
180 // Subscribe the user to the forum.
181 \mod_forum\subscriptions::subscribe_user($user->id, $forum);
183 // Retrieve all contexts - only this context should be returned.
184 $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
185 $this->assertCount(1, $contextlist);
186 $this->assertEquals($context, $contextlist->current());
188 // Export all of the data for the context.
189 $this->export_context_data_for_user($user->id, $context, 'mod_forum');
190 $writer = \core_privacy\local\request\writer::with_context($context);
191 $this->assertTrue($writer->has_any_data());
193 $subcontext = $this->get_subcontext($forum);
194 // There should be one item of metadata.
195 $this->assertCount(1, $writer->get_all_metadata($subcontext));
197 // It should be the subscriptionpreference whose value is 1.
198 $this->assertEquals(1, $writer->get_metadata($subcontext, 'subscriptionpreference'));
200 // There should be data about the forum itself.
201 $this->assertNotEmpty($writer->get_data($subcontext));
205 * Test that a user who is enrolled in a course, and who has never
206 * posted and has subscribed to the discussion will have relevant
207 * information returned.
209 public function test_user_has_never_posted_subscribed_to_discussion() {
210 // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.
211 $course = $this->getDataGenerator()->create_course();
212 $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
213 $course = $this->getDataGenerator()->create_course();
214 $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
215 $course = $this->getDataGenerator()->create_course();
216 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
217 list($user, $otheruser) = $this->helper_create_users($course, 2);
218 // Post twice - only the second discussion should be included.
219 $this->helper_post_to_forum($forum, $otheruser);
220 list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
221 $cm = get_coursemodule_from_instance('forum', $forum->id);
222 $context = \context_module::instance($cm->id);
224 // Subscribe the user to the discussion.
225 \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion);
227 // Retrieve all contexts - only this context should be returned.
228 $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
229 $this->assertCount(1, $contextlist);
230 $this->assertEquals($context, $contextlist->current());
232 // Export all of the data for the context.
233 $this->export_context_data_for_user($user->id, $context, 'mod_forum');
234 $writer = \core_privacy\local\request\writer::with_context($context);
235 $this->assertTrue($writer->has_any_data());
237 // There should be nothing in the forum. The user is not subscribed there.
238 $forumsubcontext = $this->get_subcontext($forum);
239 $this->assertCount(0, $writer->get_all_metadata($forumsubcontext));
240 $this->assert_forum_data($forum, $writer->get_data($forumsubcontext));
242 // There should be metadata in the discussion.
243 $discsubcontext = $this->get_subcontext($forum, $discussion);
244 $this->assertCount(1, $writer->get_all_metadata($discsubcontext));
246 // It should be the subscriptionpreference whose value is an Integer.
247 // (It's a timestamp, but it doesn't matter).
248 $metadata = $writer->get_metadata($discsubcontext, 'subscriptionpreference');
249 $this->assertGreaterThan(1, $metadata);
251 // For context we output the discussion content.
252 $data = $writer->get_data($discsubcontext);
253 $this->assertInstanceOf('stdClass', $data);
254 $this->assert_discussion_data($discussion, $data);
256 // Post content is not exported unless the user participated.
257 $postsubcontext = $this->get_subcontext($forum, $discussion, $post);
258 $this->assertCount(0, $writer->get_data($postsubcontext));
262 * Test that a user who has posted their own discussion will have all
265 public function test_user_has_posted_own_discussion() {
266 $course = $this->getDataGenerator()->create_course();
267 $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
268 $course = $this->getDataGenerator()->create_course();
269 $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
270 $course = $this->getDataGenerator()->create_course();
271 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
272 list($user, $otheruser) = $this->helper_create_users($course, 2);
274 // Post twice - only the second discussion should be included.
275 list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
276 list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $otheruser);
277 $cm = get_coursemodule_from_instance('forum', $forum->id);
278 $context = \context_module::instance($cm->id);
280 // Retrieve all contexts - only this context should be returned.
281 $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
282 $this->assertCount(1, $contextlist);
283 $this->assertEquals($context, $contextlist->current());
285 // Export all of the data for the context.
286 $this->setUser($user);
287 $this->export_context_data_for_user($user->id, $context, 'mod_forum');
288 $writer = \core_privacy\local\request\writer::with_context($context);
289 $this->assertTrue($writer->has_any_data());
291 // The other discussion should not have been returned as we did not post in it.
292 $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $otherdiscussion)));
294 $this->assert_discussion_data($discussion, $writer->get_data($this->get_subcontext($forum, $discussion)));
295 $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);
299 * Test that a user who has posted a reply to another users discussion
300 * will have all content returned.
302 public function test_user_has_posted_reply() {
305 // Create several courses and forums. We only insert data into the final one.
306 $course = $this->getDataGenerator()->create_course();
307 $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
308 $course = $this->getDataGenerator()->create_course();
309 $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
311 $course = $this->getDataGenerator()->create_course();
312 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
313 list($user, $otheruser) = $this->helper_create_users($course, 2);
314 // Post twice - only the second discussion should be included.
315 list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
316 list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $otheruser);
317 $cm = get_coursemodule_from_instance('forum', $forum->id);
318 $context = \context_module::instance($cm->id);
320 // Post a reply to the other person's post.
321 $reply = $this->helper_reply_to_post($post, $user);
323 // Testing as user $user.
324 $this->setUser($user);
326 // Retrieve all contexts - only this context should be returned.
327 $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
328 $this->assertCount(1, $contextlist);
329 $this->assertEquals($context, $contextlist->current());
331 // Export all of the data for the context.
332 $this->export_context_data_for_user($user->id, $context, 'mod_forum');
333 $writer = \core_privacy\local\request\writer::with_context($context);
334 $this->assertTrue($writer->has_any_data());
336 // Refresh the discussions.
337 $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
338 $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);
340 // The other discussion should not have been returned as we did not post in it.
341 $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $otherdiscussion)));
343 // Our discussion should have been returned as we did post in it.
344 $data = $writer->get_data($this->get_subcontext($forum, $discussion));
345 $this->assertNotEmpty($data);
346 $this->assert_discussion_data($discussion, $data);
348 // The reply will be included.
349 $this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer);
353 * Test that the rating of another users content will have only the
354 * rater's information returned.
356 public function test_user_has_rated_others() {
357 $course = $this->getDataGenerator()->create_course();
358 $forum = $this->getDataGenerator()->create_module('forum', [
359 'course' => $course->id,
362 list($user, $otheruser) = $this->helper_create_users($course, 2);
363 list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
364 $cm = get_coursemodule_from_instance('forum', $forum->id);
365 $context = \context_module::instance($cm->id);
367 // Rate the other users content.
368 $rm = new rating_manager();
369 $ratingoptions = new stdClass;
370 $ratingoptions->context = $context;
371 $ratingoptions->component = 'mod_forum';
372 $ratingoptions->ratingarea = 'post';
373 $ratingoptions->itemid = $post->id;
374 $ratingoptions->scaleid = $forum->scale;
375 $ratingoptions->userid = $user->id;
377 $rating = new \rating($ratingoptions);
378 $rating->update_rating(75);
380 // Run as the user under test.
381 $this->setUser($user);
383 // Retrieve all contexts - only this context should be returned.
384 $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
385 $this->assertCount(1, $contextlist);
386 $this->assertEquals($context, $contextlist->current());
388 // Export all of the data for the context.
389 $this->export_context_data_for_user($user->id, $context, 'mod_forum');
390 $writer = \core_privacy\local\request\writer::with_context($context);
391 $this->assertTrue($writer->has_any_data());
393 // The discussion should not have been returned as we did not post in it.
394 $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion)));
396 $this->assert_all_own_ratings_on_context(
399 $this->get_subcontext($forum, $discussion, $post),
405 // The original post will not be included.
406 $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);
410 * Test that ratings of a users own content will all be returned.
412 public function test_user_has_been_rated() {
413 $course = $this->getDataGenerator()->create_course();
414 $forum = $this->getDataGenerator()->create_module('forum', [
415 'course' => $course->id,
418 list($user, $otheruser, $anotheruser) = $this->helper_create_users($course, 3);
419 list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
420 $cm = get_coursemodule_from_instance('forum', $forum->id);
421 $context = \context_module::instance($cm->id);
423 // Other users rate my content.
424 $rm = new rating_manager();
425 $ratingoptions = new stdClass;
426 $ratingoptions->context = $context;
427 $ratingoptions->component = 'mod_forum';
428 $ratingoptions->ratingarea = 'post';
429 $ratingoptions->itemid = $post->id;
430 $ratingoptions->scaleid = $forum->scale;
432 $ratingoptions->userid = $otheruser->id;
433 $rating = new \rating($ratingoptions);
434 $rating->update_rating(75);
436 $ratingoptions->userid = $anotheruser->id;
437 $rating = new \rating($ratingoptions);
438 $rating->update_rating(75);
440 // Run as the user under test.
441 $this->setUser($user);
443 // Retrieve all contexts - only this context should be returned.
444 $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
445 $this->assertCount(1, $contextlist);
446 $this->assertEquals($context, $contextlist->current());
448 // Export all of the data for the context.
449 $this->export_context_data_for_user($user->id, $context, 'mod_forum');
450 $writer = \core_privacy\local\request\writer::with_context($context);
451 $this->assertTrue($writer->has_any_data());
453 $this->assert_all_ratings_on_context(
455 $this->get_subcontext($forum, $discussion, $post),
463 * Test that per-user daily digest settings are included correctly.
465 public function test_user_forum_digest() {
466 $course = $this->getDataGenerator()->create_course();
468 $forum0 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
469 $cm0 = get_coursemodule_from_instance('forum', $forum0->id);
470 $context0 = \context_module::instance($cm0->id);
472 $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
473 $cm1 = get_coursemodule_from_instance('forum', $forum1->id);
474 $context1 = \context_module::instance($cm1->id);
476 $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
477 $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
478 $context2 = \context_module::instance($cm2->id);
480 $forum3 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
481 $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
482 $context3 = \context_module::instance($cm3->id);
484 list($user) = $this->helper_create_users($course, 1);
486 // Set a digest value for each forum.
487 forum_set_user_maildigest($forum0, 0, $user);
488 forum_set_user_maildigest($forum1, 1, $user);
489 forum_set_user_maildigest($forum2, 2, $user);
491 // Run as the user under test.
492 $this->setUser($user);
494 // Retrieve all contexts - three contexts should be returned - the fourth should not be included.
495 $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
496 $this->assertCount(3, $contextlist);
504 $contextlistids = $contextlist->get_contextids();
505 sort($contextlistids);
506 $this->assertEquals($contextids, $contextlistids);
508 // Check export data for each context.
509 $this->export_context_data_for_user($user->id, $context0, 'mod_forum');
510 $this->assertEquals(0, \core_privacy\local\request\writer::with_context($context0)->get_metadata([], 'digestpreference'));
512 $this->export_context_data_for_user($user->id, $context1, 'mod_forum');
513 $this->assertEquals(1, \core_privacy\local\request\writer::with_context($context1)->get_metadata([], 'digestpreference'));
515 $this->export_context_data_for_user($user->id, $context2, 'mod_forum');
516 $this->assertEquals(2, \core_privacy\local\request\writer::with_context($context2)->get_metadata([], 'digestpreference'));
520 * Test that the per-user, per-forum user tracking data is exported.
522 public function test_user_tracking_data() {
523 $course = $this->getDataGenerator()->create_course();
525 $forumoff = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
526 $cmoff = get_coursemodule_from_instance('forum', $forumoff->id);
527 $contextoff = \context_module::instance($cmoff->id);
529 $forumon = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
530 $cmon = get_coursemodule_from_instance('forum', $forumon->id);
531 $contexton = \context_module::instance($cmon->id);
533 list($user) = $this->helper_create_users($course, 1);
535 // Set user tracking data.
536 forum_tp_stop_tracking($forumoff->id, $user->id);
537 forum_tp_start_tracking($forumon->id, $user->id);
539 // Run as the user under test.
540 $this->setUser($user);
542 // Retrieve all contexts - only the forum tracking reads should be included.
543 $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
544 $this->assertCount(1, $contextlist);
545 $this->assertEquals($contextoff, $contextlist->current());
547 // Check export data for each context.
548 $this->export_context_data_for_user($user->id, $contextoff, 'mod_forum');
549 $this->assertEquals(0,
550 \core_privacy\local\request\writer::with_context($contextoff)->get_metadata([], 'trackreadpreference'));
554 * Test that the posts which a user has read are returned correctly.
556 public function test_user_read_posts() {
559 $course = $this->getDataGenerator()->create_course();
561 $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
562 $cm1 = get_coursemodule_from_instance('forum', $forum1->id);
563 $context1 = \context_module::instance($cm1->id);
565 $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
566 $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
567 $context2 = \context_module::instance($cm2->id);
569 $forum3 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
570 $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
571 $context3 = \context_module::instance($cm3->id);
573 $forum4 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
574 $cm4 = get_coursemodule_from_instance('forum', $forum4->id);
575 $context4 = \context_module::instance($cm4->id);
577 list($author, $user) = $this->helper_create_users($course, 2);
579 list($f1d1, $f1p1) = $this->helper_post_to_forum($forum1, $author);
580 $f1p1reply = $this->helper_post_to_discussion($forum1, $f1d1, $author);
581 $f1d1 = $DB->get_record('forum_discussions', ['id' => $f1d1->id]);
582 list($f1d2, $f1p2) = $this->helper_post_to_forum($forum1, $author);
584 list($f2d1, $f2p1) = $this->helper_post_to_forum($forum2, $author);
585 $f2p1reply = $this->helper_post_to_discussion($forum2, $f2d1, $author);
586 $f2d1 = $DB->get_record('forum_discussions', ['id' => $f2d1->id]);
587 list($f2d2, $f2p2) = $this->helper_post_to_forum($forum2, $author);
589 list($f3d1, $f3p1) = $this->helper_post_to_forum($forum3, $author);
590 $f3p1reply = $this->helper_post_to_discussion($forum3, $f3d1, $author);
591 $f3d1 = $DB->get_record('forum_discussions', ['id' => $f3d1->id]);
592 list($f3d2, $f3p2) = $this->helper_post_to_forum($forum3, $author);
594 list($f4d1, $f4p1) = $this->helper_post_to_forum($forum4, $author);
595 $f4p1reply = $this->helper_post_to_discussion($forum4, $f4d1, $author);
596 $f4d1 = $DB->get_record('forum_discussions', ['id' => $f4d1->id]);
597 list($f4d2, $f4p2) = $this->helper_post_to_forum($forum4, $author);
600 // User has read post1, but not the reply or second post in forum1.
601 forum_tp_add_read_record($user->id, $f1p1->id);
603 // User has read post1 and its reply, but not the second post in forum2.
604 forum_tp_add_read_record($user->id, $f2p1->id);
605 forum_tp_add_read_record($user->id, $f2p1reply->id);
607 // User has read post2 in forum3.
608 forum_tp_add_read_record($user->id, $f3p2->id);
610 // Nothing has been read in forum4.
612 // Run as the user under test.
613 $this->setUser($user);
615 // Retrieve all contexts - should be three - forum4 has no data.
616 $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
617 $this->assertCount(3, $contextlist);
625 $contextlistids = $contextlist->get_contextids();
626 sort($contextlistids);
627 $this->assertEquals($contextids, $contextlistids);
630 $this->export_context_data_for_user($user->id, $context1, 'mod_forum');
631 $writer = \core_privacy\local\request\writer::with_context($context1);
633 // User has read f1p1.
634 $readdata = $writer->get_metadata(
635 $this->get_subcontext($forum1, $f1d1, $f1p1),
638 $this->assertNotEmpty($readdata);
639 $this->assertTrue(isset($readdata->firstread));
640 $this->assertTrue(isset($readdata->lastread));
642 // User has not f1p1reply.
643 $readdata = $writer->get_metadata(
644 $this->get_subcontext($forum1, $f1d1, $f1p1reply),
647 $this->assertEmpty($readdata);
649 // User has not f1p2.
650 $readdata = $writer->get_metadata(
651 $this->get_subcontext($forum1, $f1d2, $f1p2),
654 $this->assertEmpty($readdata);
657 $this->export_context_data_for_user($user->id, $context2, 'mod_forum');
658 $writer = \core_privacy\local\request\writer::with_context($context2);
660 // User has read f2p1.
661 $readdata = $writer->get_metadata(
662 $this->get_subcontext($forum2, $f2d1, $f2p1),
665 $this->assertNotEmpty($readdata);
666 $this->assertTrue(isset($readdata->firstread));
667 $this->assertTrue(isset($readdata->lastread));
669 // User has read f2p1reply.
670 $readdata = $writer->get_metadata(
671 $this->get_subcontext($forum2, $f2d1, $f2p1reply),
674 $this->assertNotEmpty($readdata);
675 $this->assertTrue(isset($readdata->firstread));
676 $this->assertTrue(isset($readdata->lastread));
678 // User has not read f2p2.
679 $readdata = $writer->get_metadata(
680 $this->get_subcontext($forum2, $f2d2, $f2p2),
683 $this->assertEmpty($readdata);
686 $this->export_context_data_for_user($user->id, $context3, 'mod_forum');
687 $writer = \core_privacy\local\request\writer::with_context($context3);
689 // User has not read f3p1.
690 $readdata = $writer->get_metadata(
691 $this->get_subcontext($forum3, $f3d1, $f3p1),
694 $this->assertEmpty($readdata);
696 // User has not read f3p1reply.
697 $readdata = $writer->get_metadata(
698 $this->get_subcontext($forum3, $f3d1, $f3p1reply),
701 $this->assertEmpty($readdata);
703 // User has read f3p2.
704 $readdata = $writer->get_metadata(
705 $this->get_subcontext($forum3, $f3d2, $f3p2),
708 $this->assertNotEmpty($readdata);
709 $this->assertTrue(isset($readdata->firstread));
710 $this->assertTrue(isset($readdata->lastread));
714 * Test that posts with attachments have their attachments correctly exported.
716 public function test_post_attachment_inclusion() {
719 $fs = get_file_storage();
720 $course = $this->getDataGenerator()->create_course();
721 list($author, $otheruser) = $this->helper_create_users($course, 2);
723 $forum = $this->getDataGenerator()->create_module('forum', [
724 'course' => $course->id,
727 $cm = get_coursemodule_from_instance('forum', $forum->id);
728 $context = \context_module::instance($cm->id);
730 // Create a new discussion + post in the forum.
731 list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
732 $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
734 // Add a number of replies.
735 $reply = $this->helper_reply_to_post($post, $author);
736 $reply = $this->helper_reply_to_post($post, $author);
737 $reply = $this->helper_reply_to_post($reply, $author);
738 $posts[$reply->id] = $reply;
740 // Add a fake inline image to the original post.
741 $createdfile = $fs->create_file_from_string([
742 'contextid' => $context->id,
743 'component' => 'mod_forum',
744 'filearea' => 'post',
745 'itemid' => $post->id,
747 'filename' => 'example.jpg',
749 'image contents (not really)');
751 // Tag the post and the final reply.
752 \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
753 \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $reply->id, $context, ['example', 'differenttag']);
755 // Create a second discussion + post in the forum without tags.
756 list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $author);
757 $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);
759 // Add a number of replies.
760 $reply = $this->helper_reply_to_post($otherpost, $author);
761 $reply = $this->helper_reply_to_post($otherpost, $author);
763 // Run as the user under test.
764 $this->setUser($author);
766 // Retrieve all contexts - should be one.
767 $contextlist = $this->get_contexts_for_userid($author->id, 'mod_forum');
768 $this->assertCount(1, $contextlist);
770 $this->export_context_data_for_user($author->id, $context, 'mod_forum');
771 $writer = \core_privacy\local\request\writer::with_context($context);
773 // The inline file should be on the first forum post.
774 $subcontext = $this->get_subcontext($forum, $discussion, $post);
775 $foundfiles = $writer->get_files($subcontext);
776 $this->assertCount(1, $foundfiles);
777 $this->assertEquals($createdfile, reset($foundfiles));
781 * Test that posts which include tags have those tags exported.
783 public function test_post_tags() {
786 $course = $this->getDataGenerator()->create_course();
787 list($author, $otheruser) = $this->helper_create_users($course, 2);
789 $forum = $this->getDataGenerator()->create_module('forum', [
790 'course' => $course->id,
793 $cm = get_coursemodule_from_instance('forum', $forum->id);
794 $context = \context_module::instance($cm->id);
796 // Create a new discussion + post in the forum.
797 list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
798 $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
800 // Add a number of replies.
801 $reply = $this->helper_reply_to_post($post, $author);
802 $reply = $this->helper_reply_to_post($post, $author);
803 $reply = $this->helper_reply_to_post($reply, $author);
804 $posts[$reply->id] = $reply;
806 // Tag the post and the final reply.
807 \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
808 \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $reply->id, $context, ['example', 'differenttag']);
810 // Create a second discussion + post in the forum without tags.
811 list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $author);
812 $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);
814 // Add a number of replies.
815 $reply = $this->helper_reply_to_post($otherpost, $author);
816 $reply = $this->helper_reply_to_post($otherpost, $author);
818 // Run as the user under test.
819 $this->setUser($author);
821 // Retrieve all contexts - should be two.
822 $contextlist = $this->get_contexts_for_userid($author->id, 'mod_forum');
823 $this->assertCount(1, $contextlist);
825 $this->export_all_data_for_user($author->id, 'mod_forum');
826 $writer = \core_privacy\local\request\writer::with_context($context);
828 $this->assert_all_tags_match_on_context(
831 $this->get_subcontext($forum, $discussion, $post),
839 * Ensure that all user data is deleted from a context.
841 public function test_all_users_deleted_from_context() {
844 $fs = get_file_storage();
845 $course = $this->getDataGenerator()->create_course();
846 $users = $this->helper_create_users($course, 5);
850 for ($i = 0; $i < 2; $i++) {
851 $forum = $this->getDataGenerator()->create_module('forum', [
852 'course' => $course->id,
855 $cm = get_coursemodule_from_instance('forum', $forum->id);
856 $context = \context_module::instance($cm->id);
857 $forums[$forum->id] = $forum;
858 $contexts[$forum->id] = $context;
863 foreach ($users as $user) {
864 foreach ($forums as $forum) {
865 $context = $contexts[$forum->id];
867 // Create a new discussion + post in the forum.
868 list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
869 $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
870 $discussions[$discussion->id] = $discussion;
872 // Add a number of replies.
873 $posts[$post->id] = $post;
874 $reply = $this->helper_reply_to_post($post, $user);
875 $posts[$reply->id] = $reply;
876 $reply = $this->helper_reply_to_post($post, $user);
877 $posts[$reply->id] = $reply;
878 $reply = $this->helper_reply_to_post($reply, $user);
879 $posts[$reply->id] = $reply;
881 // Add a fake inline image to the original post.
882 $fs->create_file_from_string([
883 'contextid' => $context->id,
884 'component' => 'mod_forum',
885 'filearea' => 'post',
886 'itemid' => $post->id,
888 'filename' => 'example.jpg',
889 ], 'image contents (not really)');
890 // And an attachment.
891 $fs->create_file_from_string([
892 'contextid' => $context->id,
893 'component' => 'mod_forum',
894 'filearea' => 'attachment',
895 'itemid' => $post->id,
897 'filename' => 'example.jpg',
898 ], 'image contents (not really)');
902 // Mark all posts as read by user.
903 $user = reset($users);
905 foreach ($posts as $post) {
906 $discussion = $discussions[$post->discussion];
907 $forum = $forums[$discussion->forum];
908 $context = $contexts[$forum->id];
910 // Mark the post as being read by user.
911 forum_tp_add_read_record($user->id, $post->id);
914 \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
916 // Rate the other users content.
917 if ($post->userid != $user->id) {
918 $ratedposts[$post->id] = $post;
919 $rm = new rating_manager();
920 $ratingoptions = (object) [
921 'context' => $context,
922 'component' => 'mod_forum',
923 'ratingarea' => 'post',
924 'itemid' => $post->id,
925 'scaleid' => $forum->scale,
926 'userid' => $user->id,
929 $rating = new \rating($ratingoptions);
930 $rating->update_rating(75);
934 // Run as the user under test.
935 $this->setUser($user);
937 // Retrieve all contexts - should be two.
938 $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
939 $this->assertCount(2, $contextlist);
941 // These are the contexts we expect.
942 $contextids = array_map(function($context) {
947 $contextlistids = $contextlist->get_contextids();
948 sort($contextlistids);
949 $this->assertEquals($contextids, $contextlistids);
951 // Delete for the first forum.
952 $forum = reset($forums);
953 $context = $contexts[$forum->id];
954 provider::delete_data_for_all_users_in_context($context);
956 // Determine what should have been deleted.
957 $discussionsinforum = array_filter($discussions, function($discussion) use ($forum) {
958 return $discussion->forum == $forum->id;
961 $postsinforum = array_filter($posts, function($post) use ($discussionsinforum) {
962 return isset($discussionsinforum[$post->discussion]);
965 // All forum discussions and posts should have been deleted in this forum.
966 $this->assertCount(0, $DB->get_records('forum_discussions', ['forum' => $forum->id]));
968 list ($insql, $inparams) = $DB->get_in_or_equal(array_keys($discussionsinforum));
969 $this->assertCount(0, $DB->get_records_select('forum_posts', "discussion {$insql}", $inparams));
971 // All uploaded files relating to this context should have been deleted (post content).
972 foreach ($postsinforum as $post) {
973 $this->assertEmpty($fs->get_area_files($context->id, 'mod_forum', 'post', $post->id));
974 $this->assertEmpty($fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id));
977 // All ratings should have been deleted.
978 $rm = new rating_manager();
979 foreach ($postsinforum as $post) {
980 $ratings = $rm->get_all_ratings_for_item((object) [
981 'context' => $context,
982 'component' => 'mod_forum',
983 'ratingarea' => 'post',
984 'itemid' => $post->id,
986 $this->assertEmpty($ratings);
989 // All tags should have been deleted.
990 $posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum));
991 foreach ($posttags as $tags) {
992 $this->assertEmpty($tags);
995 // Check the other forum too. It should remain intact.
996 $forum = next($forums);
997 $context = $contexts[$forum->id];
999 // Grab the list of discussions and posts in the forum.
1000 $discussionsinforum = array_filter($discussions, function($discussion) use ($forum) {
1001 return $discussion->forum == $forum->id;
1004 $postsinforum = array_filter($posts, function($post) use ($discussionsinforum) {
1005 return isset($discussionsinforum[$post->discussion]);
1008 // Forum discussions and posts should not have been deleted in this forum.
1009 $this->assertGreaterThan(0, $DB->count_records('forum_discussions', ['forum' => $forum->id]));
1011 list ($insql, $inparams) = $DB->get_in_or_equal(array_keys($discussionsinforum));
1012 $this->assertGreaterThan(0, $DB->count_records_select('forum_posts', "discussion {$insql}", $inparams));
1014 // Uploaded files relating to this context should remain.
1015 foreach ($postsinforum as $post) {
1016 if ($post->parent == 0) {
1017 $this->assertNotEmpty($fs->get_area_files($context->id, 'mod_forum', 'post', $post->id));
1021 // Ratings should not have been deleted.
1022 $rm = new rating_manager();
1023 foreach ($postsinforum as $post) {
1024 if (!isset($ratedposts[$post->id])) {
1027 $ratings = $rm->get_all_ratings_for_item((object) [
1028 'context' => $context,
1029 'component' => 'mod_forum',
1030 'ratingarea' => 'post',
1031 'itemid' => $post->id,
1033 $this->assertNotEmpty($ratings);
1036 // All tags should remain.
1037 $posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum));
1038 foreach ($posttags as $tags) {
1039 $this->assertNotEmpty($tags);
1044 * Ensure that all user data is deleted for a specific context.
1046 public function test_delete_data_for_user() {
1049 $fs = get_file_storage();
1050 $course = $this->getDataGenerator()->create_course();
1051 $users = $this->helper_create_users($course, 5);
1055 for ($i = 0; $i < 2; $i++) {
1056 $forum = $this->getDataGenerator()->create_module('forum', [
1057 'course' => $course->id,
1060 $cm = get_coursemodule_from_instance('forum', $forum->id);
1061 $context = \context_module::instance($cm->id);
1062 $forums[$forum->id] = $forum;
1063 $contexts[$forum->id] = $context;
1069 foreach ($users as $user) {
1070 $postsbyforum[$user->id] = [];
1071 foreach ($forums as $forum) {
1072 $context = $contexts[$forum->id];
1074 // Create a new discussion + post in the forum.
1075 list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
1076 $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
1077 $discussions[$discussion->id] = $discussion;
1078 $postsbyforum[$user->id][$context->id] = [];
1080 // Add a number of replies.
1081 $posts[$post->id] = $post;
1082 $thisforumposts[$post->id] = $post;
1083 $postsbyforum[$user->id][$context->id][$post->id] = $post;
1085 $reply = $this->helper_reply_to_post($post, $user);
1086 $posts[$reply->id] = $reply;
1087 $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1089 $reply = $this->helper_reply_to_post($post, $user);
1090 $posts[$reply->id] = $reply;
1091 $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1093 $reply = $this->helper_reply_to_post($reply, $user);
1094 $posts[$reply->id] = $reply;
1095 $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1097 // Add a fake inline image to the original post.
1098 $fs->create_file_from_string([
1099 'contextid' => $context->id,
1100 'component' => 'mod_forum',
1101 'filearea' => 'post',
1102 'itemid' => $post->id,
1104 'filename' => 'example.jpg',
1105 ], 'image contents (not really)');
1106 // And a fake attachment.
1107 $fs->create_file_from_string([
1108 'contextid' => $context->id,
1109 'component' => 'mod_forum',
1110 'filearea' => 'attachment',
1111 'itemid' => $post->id,
1113 'filename' => 'example.jpg',
1114 ], 'image contents (not really)');
1118 // Mark all posts as read by user1.
1119 $user1 = reset($users);
1120 foreach ($posts as $post) {
1121 $discussion = $discussions[$post->discussion];
1122 $forum = $forums[$discussion->forum];
1123 $context = $contexts[$forum->id];
1125 // Mark the post as being read by user1.
1126 forum_tp_add_read_record($user1->id, $post->id);
1129 // Rate and tag all posts.
1131 foreach ($users as $user) {
1132 foreach ($posts as $post) {
1133 $discussion = $discussions[$post->discussion];
1134 $forum = $forums[$discussion->forum];
1135 $context = $contexts[$forum->id];
1138 \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
1140 // Rate the other users content.
1141 if ($post->userid != $user->id) {
1142 $ratedposts[$post->id] = $post;
1143 $rm = new rating_manager();
1144 $ratingoptions = (object) [
1145 'context' => $context,
1146 'component' => 'mod_forum',
1147 'ratingarea' => 'post',
1148 'itemid' => $post->id,
1149 'scaleid' => $forum->scale,
1150 'userid' => $user->id,
1153 $rating = new \rating($ratingoptions);
1154 $rating->update_rating(75);
1159 // Delete for one of the forums for the first user.
1160 $firstcontext = reset($contexts);
1162 $deletedpostids = [];
1164 foreach ($postsbyforum as $user => $contexts) {
1165 foreach ($contexts as $thiscontextid => $theseposts) {
1166 $thesepostids = array_map(function($post) {
1170 if ($user == $user1->id && $thiscontextid == $firstcontext->id) {
1171 // This post is in the deleted context and by the target user.
1172 $deletedpostids = array_merge($deletedpostids, $thesepostids);
1174 // This post is by another user, or in a non-target context.
1175 $otherpostids = array_merge($otherpostids, $thesepostids);
1179 list($postinsql, $postinparams) = $DB->get_in_or_equal($deletedpostids, SQL_PARAMS_NAMED);
1180 list($otherpostinsql, $otherpostinparams) = $DB->get_in_or_equal($otherpostids, SQL_PARAMS_NAMED);
1182 $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
1183 \core_user::get_user($user1->id),
1187 provider::delete_data_for_user($approvedcontextlist);
1189 // All posts should remain.
1190 $this->assertCount(40, $DB->get_records('forum_posts'));
1192 // There should be 8 posts belonging to user1.
1193 $this->assertCount(8, $DB->get_records('forum_posts', [
1194 'userid' => $user1->id,
1197 // Four of those posts should have been marked as deleted.
1198 // That means that the deleted flag is set, and both the subject and message are empty.
1199 $this->assertCount(4, $DB->get_records_select('forum_posts', "userid = :userid AND deleted = :deleted"
1200 . " AND " . $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject')
1201 . " AND " . $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message')
1203 'userid' => $user1->id,
1209 // Only user1's posts should have been marked this way.
1210 $this->assertCount(4, $DB->get_records('forum_posts', [
1213 $this->assertCount(4, $DB->get_records_select('forum_posts',
1214 $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject'), [
1217 $this->assertCount(4, $DB->get_records_select('forum_posts',
1218 $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message'), [
1222 // Only the posts in the first discussion should have been marked this way.
1223 $this->assertCount(4, $DB->get_records_select('forum_posts',
1224 "deleted = :deleted AND id {$postinsql}",
1225 array_merge($postinparams, [
1230 // Ratings should have been removed from the affected posts.
1231 $this->assertCount(0, $DB->get_records_select('rating', "itemid {$postinsql}", $postinparams));
1233 // Ratings should remain on posts in the other context, and posts not belonging to the affected user.
1234 $this->assertCount(144, $DB->get_records_select('rating', "itemid {$otherpostinsql}", $otherpostinparams));
1236 // Ratings should remain where the user has rated another person's post.
1237 $this->assertCount(32, $DB->get_records('rating', ['userid' => $user1->id]));
1239 // Tags for the affected posts should be removed.
1240 $this->assertCount(0, $DB->get_records_select('tag_instance', "itemid {$postinsql}", $postinparams));
1242 // Tags should remain for the other posts by this user, and all posts by other users.
1243 $this->assertCount(72, $DB->get_records_select('tag_instance', "itemid {$otherpostinsql}", $otherpostinparams));
1245 // Files for the affected posts should be removed.
1246 // 5 users * 2 forums * 1 file in each forum
1247 // Original total: 10
1248 // One post with file removed.
1249 $this->assertCount(0, $DB->get_records_select('files', "itemid {$postinsql}", $postinparams));
1251 // Files for the other posts should remain.
1252 $this->assertCount(18, $DB->get_records_select('files', "filename <> '.' AND itemid {$otherpostinsql}", $otherpostinparams));