1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace Tests;
7
8use App\Events\NewPrivateNotificationEvent;
9use App\Jobs\Notifications\BeatmapsetDiscussionPostNew;
10use App\Jobs\Notifications\BroadcastNotificationBase;
11use App\Libraries\Chat;
12use App\Mail\UserNotificationDigest;
13use App\Models\Beatmapset;
14use App\Models\Notification;
15use App\Models\User;
16use App\Models\UserNotificationOption;
17use Event;
18use Mail;
19use Queue;
20use ReflectionClass;
21use ReflectionClassConstant;
22use Symfony\Component\Finder\Finder;
23
24class BroadcastNotificationTest extends TestCase
25{
26 private const IGNORED_CONST_NAMES = ['NAME_TO_CATEGORY', 'NOTIFIABLE_CLASSES', 'SUBTYPES'];
27
28 protected $sender;
29
30 public function testNoNotificationForBotUser()
31 {
32 $bot = User::factory()->withGroup('bot')->create(['user_allow_pm' => true]);
33 $this->sender->markSessionVerified();
34 $notificationsCount = Notification::count();
35
36 Chat::sendPrivateMessage($this->sender, $bot, 'hello', false);
37 $this->runFakeQueue();
38
39 $this->assertSame($notificationsCount, Notification::count());
40 Event::assertNotDispatched(NewPrivateNotificationEvent::class);
41 }
42
43 /**
44 * @dataProvider notificationNamesDataProvider
45 */
46 public function testAllNotificationNamesHaveNotificationClasses($name)
47 {
48 $this->assertNotNull(BroadcastNotificationBase::getNotificationClass($name));
49 }
50
51 /**
52 * @dataProvider notificationJobClassesDataProvider
53 */
54 public function testNotificationOptionNameHasDeliveryModes($class)
55 {
56 $predicate = $class::NOTIFICATION_OPTION_NAME === null
57 || in_array($class::NOTIFICATION_OPTION_NAME, UserNotificationOption::HAS_DELIVERY_MODES, true);
58
59 $this->assertTrue($predicate, "NOTIFICATION_OPTION_NAME for {$class} must be null or in UserNotificationOption::HAS_DELIVERY_MODES");
60 }
61
62 /**
63 * @dataProvider userNotificationDetailsDataProvider
64 */
65 public function testSendNotificationWithOptions($details)
66 {
67 $user = User::factory()->create();
68 $user->notificationOptions()->create([
69 'name' => UserNotificationOption::BEATMAPSET_MODDING,
70 'details' => $details,
71 ]);
72
73 $beatmapset = Beatmapset::factory()->owner()->withDiscussion()->create();
74 $beatmapset->watches()->create([
75 'last_read' => now()->subSeconds(), // make sure last_read isn't the same second the test runs.
76 'user_id' => $user->getKey(),
77 ]);
78
79 $this
80 ->actingAsVerified($this->sender)
81 ->post(route('beatmapsets.discussions.posts.store'), $this->makeBeatmapsetDiscussionPostParams($beatmapset, 'praise'))
82 ->assertStatus(200);
83
84 Queue::assertPushed(BeatmapsetDiscussionPostNew::class);
85 $this->runFakeQueue();
86
87 if ($details['push'] ?? BeatmapsetDiscussionPostNew::DELIVERY_MODE_DEFAULTS['push']) {
88 Event::assertDispatched(NewPrivateNotificationEvent::class);
89 } else {
90 Event::assertNotDispatched(NewPrivateNotificationEvent::class);
91 }
92
93 // make sure the mailer we want to check wasn't done by something else...
94 Mail::assertNotSent(UserNotificationDigest::class);
95 $this->artisan('notifications:send-mail');
96 $this->runFakeQueue();
97
98 if ($details['mail'] ?? BeatmapsetDiscussionPostNew::DELIVERY_MODE_DEFAULTS['mail']) {
99 Mail::assertSent(UserNotificationDigest::class);
100 } else {
101 Mail::assertNotSent(UserNotificationDigest::class);
102 }
103 }
104
105 public static function notificationJobClassesDataProvider()
106 {
107 $files = Finder::create()->files()->in(__DIR__.'/../app/Jobs/Notifications')->sortByName();
108 foreach ($files as $file) {
109 $baseName = $file->getBasename(".{$file->getExtension()}");
110 $classes[] = ["\\App\\Jobs\\Notifications\\{$baseName}"];
111 }
112
113 return $classes;
114 }
115
116 public static function notificationNamesDataProvider()
117 {
118 // TODO: move notification names to different class instead of filtering
119 $constants = collect((new ReflectionClass(Notification::class))->getReflectionConstants())
120 ->filter(fn (ReflectionClassConstant $constant) => (
121 $constant->getDeclaringClass()->name === Notification::class
122 && !in_array($constant->name, static::IGNORED_CONST_NAMES, true)
123 ))
124 ->values();
125
126 return $constants->map(fn (ReflectionClassConstant $constant) => [$constant->getValue()])->all();
127 }
128
129 public static function userNotificationDetailsDataProvider()
130 {
131 return [
132 [null], // for testing defaults.
133 [['mail' => false, 'push' => false]],
134 [['mail' => false, 'push' => true]],
135 [['mail' => true, 'push' => true]],
136 ];
137 }
138
139 protected function setUp(): void
140 {
141 parent::setUp();
142
143 // mocking the queue so we can run the job manually to get the created notification.
144 Queue::fake();
145 Event::fake();
146 Mail::fake();
147
148 $this->sender = User::factory()->create();
149 }
150}