Merge branch 'MDL-63632-master' of git://github.com/andrewnicols/moodle
[moodle.git] / mod / forum / tests / privacy_provider_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  * Tests for the forum implementation of the Privacy Provider API.
19  *
20  * @package    mod_forum
21  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 global $CFG;
29 require_once(__DIR__ . '/helper.php');
30 require_once($CFG->dirroot . '/rating/lib.php');
32 use \mod_forum\privacy\provider;
34 /**
35  * Tests for the forum implementation of the Privacy Provider API.
36  *
37  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
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.
48     use helper;
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;
56     /**
57      * Test setUp.
58      */
59     public function setUp() {
60         $this->resetAfterTest(true);
61     }
63     /**
64      * Helper to assert that the forum data is correct.
65      *
66      * @param   object  $expected The expected data in the forum.
67      * @param   object  $actual The actual data in the forum.
68      */
69     protected function assert_forum_data($expected, $actual) {
70         // Exact matches.
71         $this->assertEquals(format_string($expected->name, true), $actual->name);
72     }
74     /**
75      * Helper to assert that the discussion data is correct.
76      *
77      * @param   object  $expected The expected data in the discussion.
78      * @param   object  $actual The actual data in the discussion.
79      */
80     protected function assert_discussion_data($expected, $actual) {
81         // Exact matches.
82         $this->assertEquals(format_string($expected->name, true), $actual->name);
83         $this->assertEquals(
84             \core_privacy\local\request\transform::yesno($expected->pinned),
85             $actual->pinned
86         );
88         $this->assertEquals(
89             \core_privacy\local\request\transform::datetime($expected->timemodified),
90             $actual->timemodified
91         );
93         $this->assertEquals(
94             \core_privacy\local\request\transform::datetime($expected->usermodified),
95             $actual->usermodified
96         );
97     }
99     /**
100      * Helper to assert that the post data is correct.
101      *
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
105      */
106     protected function assert_post_data($expected, $actual, $writer) {
107         // Exact matches.
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.
112         $this->assertEquals(
113             $writer->rewrite_pluginfile_urls([], '', '', '', $expected->message),
114             $actual->message
115         );
117         $this->assertEquals(
118             \core_privacy\local\request\transform::datetime($expected->created),
119             $actual->created
120         );
122         $this->assertEquals(
123             \core_privacy\local\request\transform::datetime($expected->modified),
124             $actual->modified
125         );
126     }
128     /**
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
131      * that context.
132      */
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([]));
160     }
162     /**
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.
166      */
167     public function test_user_has_never_posted_subscribed_to_forum() {
168         global $DB;
170         // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.
171         $course = $this->getDataGenerator()->create_course();
172         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
173         $course = $this->getDataGenerator()->create_course();
174         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
175         $course = $this->getDataGenerator()->create_course();
176         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
177         list($user, $otheruser) = $this->helper_create_users($course, 2);
178         list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
179         $cm = get_coursemodule_from_instance('forum', $forum->id);
180         $context = \context_module::instance($cm->id);
182         // Subscribe the user to the forum.
183         \mod_forum\subscriptions::subscribe_user($user->id, $forum);
185         // Retrieve all contexts - only this context should be returned.
186         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
187         $this->assertCount(1, $contextlist);
188         $this->assertEquals($context, $contextlist->current());
190         // Export all of the data for the context.
191         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
192         $writer = \core_privacy\local\request\writer::with_context($context);
193         $this->assertTrue($writer->has_any_data());
195         $subcontext = $this->get_subcontext($forum);
196         // There should be one item of metadata.
197         $this->assertCount(1, $writer->get_all_metadata($subcontext));
199         // It should be the subscriptionpreference whose value is 1.
200         $this->assertEquals(1, $writer->get_metadata($subcontext, 'subscriptionpreference'));
202         // There should be data about the forum itself.
203         $this->assertNotEmpty($writer->get_data($subcontext));
205         // Delete the data now.
206         // Only the post by the user under test will be removed.
207         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
208             \core_user::get_user($user->id),
209             'mod_forum',
210             [$context->id]
211         );
212         $this->assertCount(1, $DB->get_records('forum_subscriptions', ['userid' => $user->id]));
213         provider::delete_data_for_user($approvedcontextlist);
214         $this->assertCount(0, $DB->get_records('forum_subscriptions', ['userid' => $user->id]));
215     }
217     /**
218      * Test that a user who is enrolled in a course, and who has never
219      * posted and has subscribed to the discussion will have relevant
220      * information returned.
221      */
222     public function test_user_has_never_posted_subscribed_to_discussion() {
223         global $DB;
225         // Create a course, with a forum, our user under test, another user, and a discussion + post from the other user.
226         $course = $this->getDataGenerator()->create_course();
227         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
228         $course = $this->getDataGenerator()->create_course();
229         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
230         $course = $this->getDataGenerator()->create_course();
231         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
232         list($user, $otheruser) = $this->helper_create_users($course, 2);
233         // Post twice - only the second discussion should be included.
234         $this->helper_post_to_forum($forum, $otheruser);
235         list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
236         $cm = get_coursemodule_from_instance('forum', $forum->id);
237         $context = \context_module::instance($cm->id);
239         // Subscribe the user to the discussion.
240         \mod_forum\subscriptions::subscribe_user_to_discussion($user->id, $discussion);
242         // Retrieve all contexts - only this context should be returned.
243         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
244         $this->assertCount(1, $contextlist);
245         $this->assertEquals($context, $contextlist->current());
247         // Export all of the data for the context.
248         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
249         $writer = \core_privacy\local\request\writer::with_context($context);
250         $this->assertTrue($writer->has_any_data());
252         // There should be nothing in the forum. The user is not subscribed there.
253         $forumsubcontext = $this->get_subcontext($forum);
254         $this->assertCount(0, $writer->get_all_metadata($forumsubcontext));
255         $this->assert_forum_data($forum, $writer->get_data($forumsubcontext));
257         // There should be metadata in the discussion.
258         $discsubcontext = $this->get_subcontext($forum, $discussion);
259         $this->assertCount(1, $writer->get_all_metadata($discsubcontext));
261         // It should be the subscriptionpreference whose value is an Integer.
262         // (It's a timestamp, but it doesn't matter).
263         $metadata = $writer->get_metadata($discsubcontext, 'subscriptionpreference');
264         $this->assertGreaterThan(1, $metadata);
266         // For context we output the discussion content.
267         $data = $writer->get_data($discsubcontext);
268         $this->assertInstanceOf('stdClass', $data);
269         $this->assert_discussion_data($discussion, $data);
271         // Post content is not exported unless the user participated.
272         $postsubcontext = $this->get_subcontext($forum, $discussion, $post);
273         $this->assertCount(0, $writer->get_data($postsubcontext));
275         // Delete the data now.
276         // Only the post by the user under test will be removed.
277         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
278             \core_user::get_user($user->id),
279             'mod_forum',
280             [$context->id]
281         );
282         $this->assertCount(1, $DB->get_records('forum_discussion_subs', ['userid' => $user->id]));
283         provider::delete_data_for_user($approvedcontextlist);
284         $this->assertCount(0, $DB->get_records('forum_discussion_subs', ['userid' => $user->id]));
285     }
287     /**
288      * Test that a user who has posted their own discussion will have all
289      * content returned.
290      */
291     public function test_user_has_posted_own_discussion() {
292         $course = $this->getDataGenerator()->create_course();
293         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
294         $course = $this->getDataGenerator()->create_course();
295         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
296         $course = $this->getDataGenerator()->create_course();
297         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
298         list($user, $otheruser) = $this->helper_create_users($course, 2);
300         // Post twice - only the second discussion should be included.
301         list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
302         list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $otheruser);
303         $cm = get_coursemodule_from_instance('forum', $forum->id);
304         $context = \context_module::instance($cm->id);
306         // Retrieve all contexts - only this context should be returned.
307         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
308         $this->assertCount(1, $contextlist);
309         $this->assertEquals($context, $contextlist->current());
311         // Export all of the data for the context.
312         $this->setUser($user);
313         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
314         $writer = \core_privacy\local\request\writer::with_context($context);
315         $this->assertTrue($writer->has_any_data());
317         // The other discussion should not have been returned as we did not post in it.
318         $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $otherdiscussion)));
320         $this->assert_discussion_data($discussion, $writer->get_data($this->get_subcontext($forum, $discussion)));
321         $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);
322     }
324     /**
325      * Test that a user who has posted a reply to another users discussion will have all content returned, and
326      * appropriate content removed.
327      */
328     public function test_user_has_posted_reply() {
329         global $DB;
331         // Create several courses and forums. We only insert data into the final one.
332         $course = $this->getDataGenerator()->create_course();
333         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
334         $course = $this->getDataGenerator()->create_course();
335         $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
337         $course = $this->getDataGenerator()->create_course();
338         $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
339         list($user, $otheruser) = $this->helper_create_users($course, 2);
340         // Post twice - only the second discussion should be included.
341         list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
342         list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $otheruser);
343         $cm = get_coursemodule_from_instance('forum', $forum->id);
344         $context = \context_module::instance($cm->id);
346         // Post a reply to the other person's post.
347         $reply = $this->helper_reply_to_post($post, $user);
349         // Testing as user $user.
350         $this->setUser($user);
352         // Retrieve all contexts - only this context should be returned.
353         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
354         $this->assertCount(1, $contextlist);
355         $this->assertEquals($context, $contextlist->current());
357         // Export all of the data for the context.
358         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
359         $writer = \core_privacy\local\request\writer::with_context($context);
360         $this->assertTrue($writer->has_any_data());
362         // Refresh the discussions.
363         $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
364         $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);
366         // The other discussion should not have been returned as we did not post in it.
367         $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $otherdiscussion)));
369         // Our discussion should have been returned as we did post in it.
370         $data = $writer->get_data($this->get_subcontext($forum, $discussion));
371         $this->assertNotEmpty($data);
372         $this->assert_discussion_data($discussion, $data);
374         // The reply will be included.
375         $this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer);
377         // Delete the data now.
378         // Only the post by the user under test will be removed.
379         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
380             \core_user::get_user($user->id),
381             'mod_forum',
382             [$context->id]
383         );
384         provider::delete_data_for_user($approvedcontextlist);
386         $reply = $DB->get_record('forum_posts', ['id' => $reply->id]);
387         $this->assertEmpty($reply->subject);
388         $this->assertEmpty($reply->message);
389         $this->assertEquals(1, $reply->deleted);
391         $post = $DB->get_record('forum_posts', ['id' => $post->id]);
392         $this->assertNotEmpty($post->subject);
393         $this->assertNotEmpty($post->message);
394         $this->assertEquals(0, $post->deleted);
395     }
397     /**
398      * Test that the rating of another users content will have only the
399      * rater's information returned.
400      */
401     public function test_user_has_rated_others() {
402         global $DB;
404         $course = $this->getDataGenerator()->create_course();
405         $forum = $this->getDataGenerator()->create_module('forum', [
406             'course' => $course->id,
407             'scale' => 100,
408         ]);
409         list($user, $otheruser) = $this->helper_create_users($course, 2);
410         list($discussion, $post) = $this->helper_post_to_forum($forum, $otheruser);
411         $cm = get_coursemodule_from_instance('forum', $forum->id);
412         $context = \context_module::instance($cm->id);
414         // Rate the other users content.
415         $rm = new rating_manager();
416         $ratingoptions = new stdClass;
417         $ratingoptions->context = $context;
418         $ratingoptions->component = 'mod_forum';
419         $ratingoptions->ratingarea = 'post';
420         $ratingoptions->itemid  = $post->id;
421         $ratingoptions->scaleid = $forum->scale;
422         $ratingoptions->userid  = $user->id;
424         $rating = new \rating($ratingoptions);
425         $rating->update_rating(75);
427         // Run as the user under test.
428         $this->setUser($user);
430         // Retrieve all contexts - only this context should be returned.
431         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
432         $this->assertCount(1, $contextlist);
433         $this->assertEquals($context, $contextlist->current());
435         // Export all of the data for the context.
436         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
437         $writer = \core_privacy\local\request\writer::with_context($context);
438         $this->assertTrue($writer->has_any_data());
440         // The discussion should not have been returned as we did not post in it.
441         $this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion)));
443         $this->assert_all_own_ratings_on_context(
444             $user->id,
445             $context,
446             $this->get_subcontext($forum, $discussion, $post),
447             'mod_forum',
448             'post',
449             $post->id
450         );
452         // The original post will not be included.
453         $this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);
455         // Delete the data of the user who rated the other user.
456         // The rating should not be deleted as it the rating is considered grading data.
457         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
458             \core_user::get_user($user->id),
459             'mod_forum',
460             [$context->id]
461         );
462         provider::delete_data_for_user($approvedcontextlist);
464         // Ratings should remain as they are of another user's content.
465         $this->assertCount(1, $DB->get_records('rating', ['itemid' => $post->id]));
466     }
468     /**
469      * Test that ratings of a users own content will all be returned.
470      */
471     public function test_user_has_been_rated() {
472         global $DB;
474         $course = $this->getDataGenerator()->create_course();
475         $forum = $this->getDataGenerator()->create_module('forum', [
476             'course' => $course->id,
477             'scale' => 100,
478         ]);
479         list($user, $otheruser, $anotheruser) = $this->helper_create_users($course, 3);
480         list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
481         $cm = get_coursemodule_from_instance('forum', $forum->id);
482         $context = \context_module::instance($cm->id);
484         // Other users rate my content.
485         $rm = new rating_manager();
486         $ratingoptions = new stdClass;
487         $ratingoptions->context = $context;
488         $ratingoptions->component = 'mod_forum';
489         $ratingoptions->ratingarea = 'post';
490         $ratingoptions->itemid  = $post->id;
491         $ratingoptions->scaleid = $forum->scale;
493         $ratingoptions->userid  = $otheruser->id;
494         $rating = new \rating($ratingoptions);
495         $rating->update_rating(75);
497         $ratingoptions->userid  = $anotheruser->id;
498         $rating = new \rating($ratingoptions);
499         $rating->update_rating(75);
501         // Run as the user under test.
502         $this->setUser($user);
504         // Retrieve all contexts - only this context should be returned.
505         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
506         $this->assertCount(1, $contextlist);
507         $this->assertEquals($context, $contextlist->current());
509         // Export all of the data for the context.
510         $this->export_context_data_for_user($user->id, $context, 'mod_forum');
511         $writer = \core_privacy\local\request\writer::with_context($context);
512         $this->assertTrue($writer->has_any_data());
514         $this->assert_all_ratings_on_context(
515             $context,
516             $this->get_subcontext($forum, $discussion, $post),
517             'mod_forum',
518             'post',
519             $post->id
520         );
522         // Delete the data of the user who was rated.
523         // The rating should now be deleted.
524         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
525             \core_user::get_user($user->id),
526             'mod_forum',
527             [$context->id]
528         );
529         provider::delete_data_for_user($approvedcontextlist);
531         // Ratings should remain as they are of another user's content.
532         $this->assertCount(0, $DB->get_records('rating', ['itemid' => $post->id]));
533     }
535     /**
536      * Test that per-user daily digest settings are included correctly.
537      */
538     public function test_user_forum_digest() {
539         global $DB;
541         $course = $this->getDataGenerator()->create_course();
543         $forum0 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
544         $cm0 = get_coursemodule_from_instance('forum', $forum0->id);
545         $context0 = \context_module::instance($cm0->id);
547         $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
548         $cm1 = get_coursemodule_from_instance('forum', $forum1->id);
549         $context1 = \context_module::instance($cm1->id);
551         $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
552         $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
553         $context2 = \context_module::instance($cm2->id);
555         $forum3 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
556         $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
557         $context3 = \context_module::instance($cm3->id);
559         list($user) = $this->helper_create_users($course, 1);
561         // Set a digest value for each forum.
562         forum_set_user_maildigest($forum0, 0, $user);
563         forum_set_user_maildigest($forum1, 1, $user);
564         forum_set_user_maildigest($forum2, 2, $user);
566         // Run as the user under test.
567         $this->setUser($user);
569         // Retrieve all contexts - three contexts should be returned - the fourth should not be included.
570         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
571         $this->assertCount(3, $contextlist);
573         $contextids = [
574                 $context0->id,
575                 $context1->id,
576                 $context2->id,
577             ];
578         sort($contextids);
579         $contextlistids = $contextlist->get_contextids();
580         sort($contextlistids);
581         $this->assertEquals($contextids, $contextlistids);
583         // Check export data for each context.
584         $this->export_context_data_for_user($user->id, $context0, 'mod_forum');
585         $this->assertEquals(0, \core_privacy\local\request\writer::with_context($context0)->get_metadata([], 'digestpreference'));
587         $this->export_context_data_for_user($user->id, $context1, 'mod_forum');
588         $this->assertEquals(1, \core_privacy\local\request\writer::with_context($context1)->get_metadata([], 'digestpreference'));
590         $this->export_context_data_for_user($user->id, $context2, 'mod_forum');
591         $this->assertEquals(2, \core_privacy\local\request\writer::with_context($context2)->get_metadata([], 'digestpreference'));
593         // Delete the data for one of the users in one of the forums.
594         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
595             \core_user::get_user($user->id),
596             'mod_forum',
597             [$context1->id]
598         );
600         $this->assertEquals(0, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum0->id]));
601         $this->assertEquals(1, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum1->id]));
602         $this->assertEquals(2, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum2->id]));
603         provider::delete_data_for_user($approvedcontextlist);
604         $this->assertEquals(0, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum0->id]));
605         $this->assertFalse($DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum1->id]));
606         $this->assertEquals(2, $DB->get_field('forum_digests', 'maildigest', ['userid' => $user->id, 'forum' => $forum2->id]));
608     }
610     /**
611      * Test that the per-user, per-forum user tracking data is exported.
612      */
613     public function test_user_tracking_data() {
614         global $DB;
616         $course = $this->getDataGenerator()->create_course();
618         $forumoff = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
619         $cmoff = get_coursemodule_from_instance('forum', $forumoff->id);
620         $contextoff = \context_module::instance($cmoff->id);
622         $forumon = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
623         $cmon = get_coursemodule_from_instance('forum', $forumon->id);
624         $contexton = \context_module::instance($cmon->id);
626         list($user) = $this->helper_create_users($course, 1);
628         // Set user tracking data.
629         forum_tp_stop_tracking($forumoff->id, $user->id);
630         forum_tp_start_tracking($forumon->id, $user->id);
632         // Run as the user under test.
633         $this->setUser($user);
635         // Retrieve all contexts - only the forum tracking reads should be included.
636         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
637         $this->assertCount(1, $contextlist);
638         $this->assertEquals($contextoff, $contextlist->current());
640         // Check export data for each context.
641         $this->export_context_data_for_user($user->id, $contextoff, 'mod_forum');
642         $this->assertEquals(0,
643                 \core_privacy\local\request\writer::with_context($contextoff)->get_metadata([], 'trackreadpreference'));
645         // Delete the data for one of the users in the 'on' forum.
646         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
647             \core_user::get_user($user->id),
648             'mod_forum',
649             [$contexton->id]
650         );
652         $this->assertTrue($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumoff->id]));
653         $this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumon->id]));
655         provider::delete_data_for_user($approvedcontextlist);
657         $this->assertTrue($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumoff->id]));
658         $this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumon->id]));
660         // Delete the data for one of the users in the 'off' forum.
661         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
662             \core_user::get_user($user->id),
663             'mod_forum',
664             [$contextoff->id]
665         );
667         provider::delete_data_for_user($approvedcontextlist);
669         $this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumoff->id]));
670         $this->assertFalse($DB->record_exists('forum_track_prefs', ['userid' => $user->id, 'forumid' => $forumon->id]));
671     }
673     /**
674      * Test that the posts which a user has read are returned correctly.
675      */
676     public function test_user_read_posts() {
677         global $DB;
679         $course = $this->getDataGenerator()->create_course();
681         $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
682         $cm1 = get_coursemodule_from_instance('forum', $forum1->id);
683         $context1 = \context_module::instance($cm1->id);
685         $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
686         $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
687         $context2 = \context_module::instance($cm2->id);
689         $forum3 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
690         $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
691         $context3 = \context_module::instance($cm3->id);
693         $forum4 = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
694         $cm4 = get_coursemodule_from_instance('forum', $forum4->id);
695         $context4 = \context_module::instance($cm4->id);
697         list($author, $user) = $this->helper_create_users($course, 2);
699         list($f1d1, $f1p1) = $this->helper_post_to_forum($forum1, $author);
700         $f1p1reply = $this->helper_post_to_discussion($forum1, $f1d1, $author);
701         $f1d1 = $DB->get_record('forum_discussions', ['id' => $f1d1->id]);
702         list($f1d2, $f1p2) = $this->helper_post_to_forum($forum1, $author);
704         list($f2d1, $f2p1) = $this->helper_post_to_forum($forum2, $author);
705         $f2p1reply = $this->helper_post_to_discussion($forum2, $f2d1, $author);
706         $f2d1 = $DB->get_record('forum_discussions', ['id' => $f2d1->id]);
707         list($f2d2, $f2p2) = $this->helper_post_to_forum($forum2, $author);
709         list($f3d1, $f3p1) = $this->helper_post_to_forum($forum3, $author);
710         $f3p1reply = $this->helper_post_to_discussion($forum3, $f3d1, $author);
711         $f3d1 = $DB->get_record('forum_discussions', ['id' => $f3d1->id]);
712         list($f3d2, $f3p2) = $this->helper_post_to_forum($forum3, $author);
714         list($f4d1, $f4p1) = $this->helper_post_to_forum($forum4, $author);
715         $f4p1reply = $this->helper_post_to_discussion($forum4, $f4d1, $author);
716         $f4d1 = $DB->get_record('forum_discussions', ['id' => $f4d1->id]);
717         list($f4d2, $f4p2) = $this->helper_post_to_forum($forum4, $author);
719         // Insert read info.
720         // User has read post1, but not the reply or second post in forum1.
721         forum_tp_add_read_record($user->id, $f1p1->id);
723         // User has read post1 and its reply, but not the second post in forum2.
724         forum_tp_add_read_record($user->id, $f2p1->id);
725         forum_tp_add_read_record($user->id, $f2p1reply->id);
727         // User has read post2 in forum3.
728         forum_tp_add_read_record($user->id, $f3p2->id);
730         // Nothing has been read in forum4.
732         // Run as the user under test.
733         $this->setUser($user);
735         // Retrieve all contexts - should be three - forum4 has no data.
736         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
737         $this->assertCount(3, $contextlist);
739         $contextids = [
740                 $context1->id,
741                 $context2->id,
742                 $context3->id,
743             ];
744         sort($contextids);
745         $contextlistids = $contextlist->get_contextids();
746         sort($contextlistids);
747         $this->assertEquals($contextids, $contextlistids);
749         // Forum 1.
750         $this->export_context_data_for_user($user->id, $context1, 'mod_forum');
751         $writer = \core_privacy\local\request\writer::with_context($context1);
753         // User has read f1p1.
754         $readdata = $writer->get_metadata(
755                 $this->get_subcontext($forum1, $f1d1, $f1p1),
756                 'postread'
757             );
758         $this->assertNotEmpty($readdata);
759         $this->assertTrue(isset($readdata->firstread));
760         $this->assertTrue(isset($readdata->lastread));
762         // User has not f1p1reply.
763         $readdata = $writer->get_metadata(
764                 $this->get_subcontext($forum1, $f1d1, $f1p1reply),
765                 'postread'
766             );
767         $this->assertEmpty($readdata);
769         // User has not f1p2.
770         $readdata = $writer->get_metadata(
771                 $this->get_subcontext($forum1, $f1d2, $f1p2),
772                 'postread'
773             );
774         $this->assertEmpty($readdata);
776         // Forum 2.
777         $this->export_context_data_for_user($user->id, $context2, 'mod_forum');
778         $writer = \core_privacy\local\request\writer::with_context($context2);
780         // User has read f2p1.
781         $readdata = $writer->get_metadata(
782                 $this->get_subcontext($forum2, $f2d1, $f2p1),
783                 'postread'
784             );
785         $this->assertNotEmpty($readdata);
786         $this->assertTrue(isset($readdata->firstread));
787         $this->assertTrue(isset($readdata->lastread));
789         // User has read f2p1reply.
790         $readdata = $writer->get_metadata(
791                 $this->get_subcontext($forum2, $f2d1, $f2p1reply),
792                 'postread'
793             );
794         $this->assertNotEmpty($readdata);
795         $this->assertTrue(isset($readdata->firstread));
796         $this->assertTrue(isset($readdata->lastread));
798         // User has not read f2p2.
799         $readdata = $writer->get_metadata(
800                 $this->get_subcontext($forum2, $f2d2, $f2p2),
801                 'postread'
802             );
803         $this->assertEmpty($readdata);
805         // Forum 3.
806         $this->export_context_data_for_user($user->id, $context3, 'mod_forum');
807         $writer = \core_privacy\local\request\writer::with_context($context3);
809         // User has not read f3p1.
810         $readdata = $writer->get_metadata(
811                 $this->get_subcontext($forum3, $f3d1, $f3p1),
812                 'postread'
813             );
814         $this->assertEmpty($readdata);
816         // User has not read f3p1reply.
817         $readdata = $writer->get_metadata(
818                 $this->get_subcontext($forum3, $f3d1, $f3p1reply),
819                 'postread'
820             );
821         $this->assertEmpty($readdata);
823         // User has read f3p2.
824         $readdata = $writer->get_metadata(
825                 $this->get_subcontext($forum3, $f3d2, $f3p2),
826                 'postread'
827             );
828         $this->assertNotEmpty($readdata);
829         $this->assertTrue(isset($readdata->firstread));
830         $this->assertTrue(isset($readdata->lastread));
832         // Delete all data for one of the users in one of the forums.
833         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
834             \core_user::get_user($user->id),
835             'mod_forum',
836             [$context3->id]
837         );
839         $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum1->id]));
840         $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum2->id]));
841         $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum3->id]));
843         provider::delete_data_for_user($approvedcontextlist);
845         $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum1->id]));
846         $this->assertTrue($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum2->id]));
847         $this->assertFalse($DB->record_exists('forum_read', ['userid' => $user->id, 'forumid' => $forum3->id]));
848     }
850     /**
851      * Test that posts with attachments have their attachments correctly exported.
852      */
853     public function test_post_attachment_inclusion() {
854         global $DB;
856         $fs = get_file_storage();
857         $course = $this->getDataGenerator()->create_course();
858         list($author, $otheruser) = $this->helper_create_users($course, 2);
860         $forum = $this->getDataGenerator()->create_module('forum', [
861             'course' => $course->id,
862             'scale' => 100,
863         ]);
864         $cm = get_coursemodule_from_instance('forum', $forum->id);
865         $context = \context_module::instance($cm->id);
867         // Create a new discussion + post in the forum.
868         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
869         $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
871         // Add a number of replies.
872         $reply = $this->helper_reply_to_post($post, $author);
873         $reply = $this->helper_reply_to_post($post, $author);
874         $reply = $this->helper_reply_to_post($reply, $author);
875         $posts[$reply->id] = $reply;
877         // Add a fake inline image to the original post.
878         $createdfile = $fs->create_file_from_string([
879                 'contextid' => $context->id,
880                 'component' => 'mod_forum',
881                 'filearea'  => 'post',
882                 'itemid'    => $post->id,
883                 'filepath'  => '/',
884                 'filename'  => 'example.jpg',
885             ],
886         'image contents (not really)');
888         // Tag the post and the final reply.
889         \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
890         \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $reply->id, $context, ['example', 'differenttag']);
892         // Create a second discussion + post in the forum without tags.
893         list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $author);
894         $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);
896         // Add a number of replies.
897         $reply = $this->helper_reply_to_post($otherpost, $author);
898         $reply = $this->helper_reply_to_post($otherpost, $author);
900         // Run as the user under test.
901         $this->setUser($author);
903         // Retrieve all contexts - should be one.
904         $contextlist = $this->get_contexts_for_userid($author->id, 'mod_forum');
905         $this->assertCount(1, $contextlist);
907         $this->export_context_data_for_user($author->id, $context, 'mod_forum');
908         $writer = \core_privacy\local\request\writer::with_context($context);
910         // The inline file should be on the first forum post.
911         $subcontext = $this->get_subcontext($forum, $discussion, $post);
912         $foundfiles = $writer->get_files($subcontext);
913         $this->assertCount(1, $foundfiles);
914         $this->assertEquals($createdfile, reset($foundfiles));
915     }
917     /**
918      * Test that posts which include tags have those tags exported.
919      */
920     public function test_post_tags() {
921         global $DB;
923         $course = $this->getDataGenerator()->create_course();
924         list($author, $otheruser) = $this->helper_create_users($course, 2);
926         $forum = $this->getDataGenerator()->create_module('forum', [
927             'course' => $course->id,
928             'scale' => 100,
929         ]);
930         $cm = get_coursemodule_from_instance('forum', $forum->id);
931         $context = \context_module::instance($cm->id);
933         // Create a new discussion + post in the forum.
934         list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
935         $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
937         // Add a number of replies.
938         $reply = $this->helper_reply_to_post($post, $author);
939         $reply = $this->helper_reply_to_post($post, $author);
940         $reply = $this->helper_reply_to_post($reply, $author);
941         $posts[$reply->id] = $reply;
943         // Tag the post and the final reply.
944         \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
945         \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $reply->id, $context, ['example', 'differenttag']);
947         // Create a second discussion + post in the forum without tags.
948         list($otherdiscussion, $otherpost) = $this->helper_post_to_forum($forum, $author);
949         $otherdiscussion = $DB->get_record('forum_discussions', ['id' => $otherdiscussion->id]);
951         // Add a number of replies.
952         $reply = $this->helper_reply_to_post($otherpost, $author);
953         $reply = $this->helper_reply_to_post($otherpost, $author);
955         // Run as the user under test.
956         $this->setUser($author);
958         // Retrieve all contexts - should be two.
959         $contextlist = $this->get_contexts_for_userid($author->id, 'mod_forum');
960         $this->assertCount(1, $contextlist);
962         $this->export_all_data_for_user($author->id, 'mod_forum');
963         $writer = \core_privacy\local\request\writer::with_context($context);
965         $this->assert_all_tags_match_on_context(
966             $author->id,
967             $context,
968             $this->get_subcontext($forum, $discussion, $post),
969             'mod_forum',
970             'forum_posts',
971             $post->id
972         );
973     }
975     /**
976      * Ensure that all user data is deleted from a context.
977      */
978     public function test_all_users_deleted_from_context() {
979         global $DB;
981         $fs = get_file_storage();
982         $course = $this->getDataGenerator()->create_course();
983         $users = $this->helper_create_users($course, 5);
985         $forums = [];
986         $contexts = [];
987         for ($i = 0; $i < 2; $i++) {
988             $forum = $this->getDataGenerator()->create_module('forum', [
989                 'course' => $course->id,
990                 'scale' => 100,
991             ]);
992             $cm = get_coursemodule_from_instance('forum', $forum->id);
993             $context = \context_module::instance($cm->id);
994             $forums[$forum->id] = $forum;
995             $contexts[$forum->id] = $context;
996         }
998         $discussions = [];
999         $posts = [];
1000         foreach ($users as $user) {
1001             foreach ($forums as $forum) {
1002                 $context = $contexts[$forum->id];
1004                 // Create a new discussion + post in the forum.
1005                 list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
1006                 $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
1007                 $discussions[$discussion->id] = $discussion;
1009                 // Add a number of replies.
1010                 $posts[$post->id] = $post;
1011                 $reply = $this->helper_reply_to_post($post, $user);
1012                 $posts[$reply->id] = $reply;
1013                 $reply = $this->helper_reply_to_post($post, $user);
1014                 $posts[$reply->id] = $reply;
1015                 $reply = $this->helper_reply_to_post($reply, $user);
1016                 $posts[$reply->id] = $reply;
1018                 // Add a fake inline image to the original post.
1019                 $fs->create_file_from_string([
1020                         'contextid' => $context->id,
1021                         'component' => 'mod_forum',
1022                         'filearea'  => 'post',
1023                         'itemid'    => $post->id,
1024                         'filepath'  => '/',
1025                         'filename'  => 'example.jpg',
1026                     ], 'image contents (not really)');
1027                 // And an attachment.
1028                 $fs->create_file_from_string([
1029                         'contextid' => $context->id,
1030                         'component' => 'mod_forum',
1031                         'filearea'  => 'attachment',
1032                         'itemid'    => $post->id,
1033                         'filepath'  => '/',
1034                         'filename'  => 'example.jpg',
1035                     ], 'image contents (not really)');
1036             }
1037         }
1039         // Mark all posts as read by user.
1040         $user = reset($users);
1041         $ratedposts = [];
1042         foreach ($posts as $post) {
1043             $discussion = $discussions[$post->discussion];
1044             $forum = $forums[$discussion->forum];
1045             $context = $contexts[$forum->id];
1047             // Mark the post as being read by user.
1048             forum_tp_add_read_record($user->id, $post->id);
1050             // Tag the post.
1051             \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
1053             // Rate the other users content.
1054             if ($post->userid != $user->id) {
1055                 $ratedposts[$post->id] = $post;
1056                 $rm = new rating_manager();
1057                 $ratingoptions = (object) [
1058                     'context' => $context,
1059                     'component' => 'mod_forum',
1060                     'ratingarea' => 'post',
1061                     'itemid' => $post->id,
1062                     'scaleid' => $forum->scale,
1063                     'userid' => $user->id,
1064                 ];
1066                 $rating = new \rating($ratingoptions);
1067                 $rating->update_rating(75);
1068             }
1069         }
1071         // Run as the user under test.
1072         $this->setUser($user);
1074         // Retrieve all contexts - should be two.
1075         $contextlist = $this->get_contexts_for_userid($user->id, 'mod_forum');
1076         $this->assertCount(2, $contextlist);
1078         // These are the contexts we expect.
1079         $contextids = array_map(function($context) {
1080             return $context->id;
1081         }, $contexts);
1082         sort($contextids);
1084         $contextlistids = $contextlist->get_contextids();
1085         sort($contextlistids);
1086         $this->assertEquals($contextids, $contextlistids);
1088         // Delete for the first forum.
1089         $forum = reset($forums);
1090         $context = $contexts[$forum->id];
1091         provider::delete_data_for_all_users_in_context($context);
1093         // Determine what should have been deleted.
1094         $discussionsinforum = array_filter($discussions, function($discussion) use ($forum) {
1095             return $discussion->forum == $forum->id;
1096         });
1098         $postsinforum = array_filter($posts, function($post) use ($discussionsinforum) {
1099             return isset($discussionsinforum[$post->discussion]);
1100         });
1102         // All forum discussions and posts should have been deleted in this forum.
1103         $this->assertCount(0, $DB->get_records('forum_discussions', ['forum' => $forum->id]));
1105         list ($insql, $inparams) = $DB->get_in_or_equal(array_keys($discussionsinforum));
1106         $this->assertCount(0, $DB->get_records_select('forum_posts', "discussion {$insql}", $inparams));
1108         // All uploaded files relating to this context should have been deleted (post content).
1109         foreach ($postsinforum as $post) {
1110             $this->assertEmpty($fs->get_area_files($context->id, 'mod_forum', 'post', $post->id));
1111             $this->assertEmpty($fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id));
1112         }
1114         // All ratings should have been deleted.
1115         $rm = new rating_manager();
1116         foreach ($postsinforum as $post) {
1117             $ratings = $rm->get_all_ratings_for_item((object) [
1118                 'context' => $context,
1119                 'component' => 'mod_forum',
1120                 'ratingarea' => 'post',
1121                 'itemid' => $post->id,
1122             ]);
1123             $this->assertEmpty($ratings);
1124         }
1126         // All tags should have been deleted.
1127         $posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum));
1128         foreach ($posttags as $tags) {
1129             $this->assertEmpty($tags);
1130         }
1132         // Check the other forum too. It should remain intact.
1133         $forum = next($forums);
1134         $context = $contexts[$forum->id];
1136         // Grab the list of discussions and posts in the forum.
1137         $discussionsinforum = array_filter($discussions, function($discussion) use ($forum) {
1138             return $discussion->forum == $forum->id;
1139         });
1141         $postsinforum = array_filter($posts, function($post) use ($discussionsinforum) {
1142             return isset($discussionsinforum[$post->discussion]);
1143         });
1145         // Forum discussions and posts should not have been deleted in this forum.
1146         $this->assertGreaterThan(0, $DB->count_records('forum_discussions', ['forum' => $forum->id]));
1148         list ($insql, $inparams) = $DB->get_in_or_equal(array_keys($discussionsinforum));
1149         $this->assertGreaterThan(0, $DB->count_records_select('forum_posts', "discussion {$insql}", $inparams));
1151         // Uploaded files relating to this context should remain.
1152         foreach ($postsinforum as $post) {
1153             if ($post->parent == 0) {
1154                 $this->assertNotEmpty($fs->get_area_files($context->id, 'mod_forum', 'post', $post->id));
1155             }
1156         }
1158         // Ratings should not have been deleted.
1159         $rm = new rating_manager();
1160         foreach ($postsinforum as $post) {
1161             if (!isset($ratedposts[$post->id])) {
1162                 continue;
1163             }
1164             $ratings = $rm->get_all_ratings_for_item((object) [
1165                 'context' => $context,
1166                 'component' => 'mod_forum',
1167                 'ratingarea' => 'post',
1168                 'itemid' => $post->id,
1169             ]);
1170             $this->assertNotEmpty($ratings);
1171         }
1173         // All tags should remain.
1174         $posttags = \core_tag_tag::get_items_tags('mod_forum', 'forum_posts', array_keys($postsinforum));
1175         foreach ($posttags as $tags) {
1176             $this->assertNotEmpty($tags);
1177         }
1178     }
1180     /**
1181      * Ensure that all user data is deleted for a specific context.
1182      */
1183     public function test_delete_data_for_user() {
1184         global $DB;
1186         $fs = get_file_storage();
1187         $course = $this->getDataGenerator()->create_course();
1188         $users = $this->helper_create_users($course, 5);
1190         $forums = [];
1191         $contexts = [];
1192         for ($i = 0; $i < 2; $i++) {
1193             $forum = $this->getDataGenerator()->create_module('forum', [
1194                 'course' => $course->id,
1195                 'scale' => 100,
1196             ]);
1197             $cm = get_coursemodule_from_instance('forum', $forum->id);
1198             $context = \context_module::instance($cm->id);
1199             $forums[$forum->id] = $forum;
1200             $contexts[$forum->id] = $context;
1201         }
1203         $discussions = [];
1204         $posts = [];
1205         $postsbyforum = [];
1206         foreach ($users as $user) {
1207             $postsbyforum[$user->id] = [];
1208             foreach ($forums as $forum) {
1209                 $context = $contexts[$forum->id];
1211                 // Create a new discussion + post in the forum.
1212                 list($discussion, $post) = $this->helper_post_to_forum($forum, $user);
1213                 $discussion = $DB->get_record('forum_discussions', ['id' => $discussion->id]);
1214                 $discussions[$discussion->id] = $discussion;
1215                 $postsbyforum[$user->id][$context->id] = [];
1217                 // Add a number of replies.
1218                 $posts[$post->id] = $post;
1219                 $thisforumposts[$post->id] = $post;
1220                 $postsbyforum[$user->id][$context->id][$post->id] = $post;
1222                 $reply = $this->helper_reply_to_post($post, $user);
1223                 $posts[$reply->id] = $reply;
1224                 $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1226                 $reply = $this->helper_reply_to_post($post, $user);
1227                 $posts[$reply->id] = $reply;
1228                 $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1230                 $reply = $this->helper_reply_to_post($reply, $user);
1231                 $posts[$reply->id] = $reply;
1232                 $postsbyforum[$user->id][$context->id][$reply->id] = $reply;
1234                 // Add a fake inline image to the original post.
1235                 $fs->create_file_from_string([
1236                         'contextid' => $context->id,
1237                         'component' => 'mod_forum',
1238                         'filearea'  => 'post',
1239                         'itemid'    => $post->id,
1240                         'filepath'  => '/',
1241                         'filename'  => 'example.jpg',
1242                     ], 'image contents (not really)');
1243                 // And a fake attachment.
1244                 $fs->create_file_from_string([
1245                         'contextid' => $context->id,
1246                         'component' => 'mod_forum',
1247                         'filearea'  => 'attachment',
1248                         'itemid'    => $post->id,
1249                         'filepath'  => '/',
1250                         'filename'  => 'example.jpg',
1251                     ], 'image contents (not really)');
1252             }
1253         }
1255         // Mark all posts as read by user1.
1256         $user1 = reset($users);
1257         foreach ($posts as $post) {
1258             $discussion = $discussions[$post->discussion];
1259             $forum = $forums[$discussion->forum];
1260             $context = $contexts[$forum->id];
1262             // Mark the post as being read by user1.
1263             forum_tp_add_read_record($user1->id, $post->id);
1264         }
1266         // Rate and tag all posts.
1267         $ratedposts = [];
1268         foreach ($users as $user) {
1269             foreach ($posts as $post) {
1270                 $discussion = $discussions[$post->discussion];
1271                 $forum = $forums[$discussion->forum];
1272                 $context = $contexts[$forum->id];
1274                 // Tag the post.
1275                 \core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, ['example', 'tag']);
1277                 // Rate the other users content.
1278                 if ($post->userid != $user->id) {
1279                     $ratedposts[$post->id] = $post;
1280                     $rm = new rating_manager();
1281                     $ratingoptions = (object) [
1282                         'context' => $context,
1283                         'component' => 'mod_forum',
1284                         'ratingarea' => 'post',
1285                         'itemid' => $post->id,
1286                         'scaleid' => $forum->scale,
1287                         'userid' => $user->id,
1288                     ];
1290                     $rating = new \rating($ratingoptions);
1291                     $rating->update_rating(75);
1292                 }
1293             }
1294         }
1296         // Delete for one of the forums for the first user.
1297         $firstcontext = reset($contexts);
1299         $deletedpostids = [];
1300         $otherpostids = [];
1301         foreach ($postsbyforum as $user => $contexts) {
1302             foreach ($contexts as $thiscontextid => $theseposts) {
1303                 $thesepostids = array_map(function($post) {
1304                     return $post->id;
1305                 }, $theseposts);
1307                 if ($user == $user1->id && $thiscontextid == $firstcontext->id) {
1308                     // This post is in the deleted context and by the target user.
1309                     $deletedpostids = array_merge($deletedpostids, $thesepostids);
1310                 } else {
1311                     // This post is by another user, or in a non-target context.
1312                     $otherpostids = array_merge($otherpostids, $thesepostids);
1313                 }
1314             }
1315         }
1316         list($postinsql, $postinparams) = $DB->get_in_or_equal($deletedpostids, SQL_PARAMS_NAMED);
1317         list($otherpostinsql, $otherpostinparams) = $DB->get_in_or_equal($otherpostids, SQL_PARAMS_NAMED);
1319         $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
1320             \core_user::get_user($user1->id),
1321             'mod_forum',
1322             [$firstcontext->id]
1323         );
1324         provider::delete_data_for_user($approvedcontextlist);
1326         // All posts should remain.
1327         $this->assertCount(40, $DB->get_records('forum_posts'));
1329         // There should be 8 posts belonging to user1.
1330         $this->assertCount(8, $DB->get_records('forum_posts', [
1331                 'userid' => $user1->id,
1332             ]));
1334         // Four of those posts should have been marked as deleted.
1335         // That means that the deleted flag is set, and both the subject and message are empty.
1336         $this->assertCount(4, $DB->get_records_select('forum_posts', "userid = :userid AND deleted = :deleted"
1337                     . " AND " . $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject')
1338                     . " AND " . $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message')
1339                 , [
1340                     'userid' => $user1->id,
1341                     'deleted' => 1,
1342                     'subject' => '',
1343                     'message' => '',
1344                 ]));
1346         // Only user1's posts should have been marked this way.
1347         $this->assertCount(4, $DB->get_records('forum_posts', [
1348                 'deleted' => 1,
1349             ]));
1350         $this->assertCount(4, $DB->get_records_select('forum_posts',
1351             $DB->sql_compare_text('subject') . " = " . $DB->sql_compare_text(':subject'), [
1352                 'subject' => '',
1353             ]));
1354         $this->assertCount(4, $DB->get_records_select('forum_posts',
1355             $DB->sql_compare_text('message') . " = " . $DB->sql_compare_text(':message'), [
1356                 'message' => '',
1357             ]));
1359         // Only the posts in the first discussion should have been marked this way.
1360         $this->assertCount(4, $DB->get_records_select('forum_posts',
1361             "deleted = :deleted AND id {$postinsql}",
1362                 array_merge($postinparams, [
1363                     'deleted' => 1,
1364                 ])
1365             ));
1367         // Ratings should have been removed from the affected posts.
1368         $this->assertCount(0, $DB->get_records_select('rating', "itemid {$postinsql}", $postinparams));
1370         // Ratings should remain on posts in the other context, and posts not belonging to the affected user.
1371         $this->assertCount(144, $DB->get_records_select('rating', "itemid {$otherpostinsql}", $otherpostinparams));
1373         // Ratings should remain where the user has rated another person's post.
1374         $this->assertCount(32, $DB->get_records('rating', ['userid' => $user1->id]));
1376         // Tags for the affected posts should be removed.
1377         $this->assertCount(0, $DB->get_records_select('tag_instance', "itemid {$postinsql}", $postinparams));
1379         // Tags should remain for the other posts by this user, and all posts by other users.
1380         $this->assertCount(72, $DB->get_records_select('tag_instance', "itemid {$otherpostinsql}", $otherpostinparams));
1382         // Files for the affected posts should be removed.
1383         // 5 users * 2 forums * 1 file in each forum
1384         // Original total: 10
1385         // One post with file removed.
1386         $this->assertCount(0, $DB->get_records_select('files', "itemid {$postinsql}", $postinparams));
1388         // Files for the other posts should remain.
1389         $this->assertCount(18, $DB->get_records_select('files', "filename <> '.' AND itemid {$otherpostinsql}", $otherpostinparams));
1390     }