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
6declare(strict_types=1);
7
8namespace Tests\Controllers\Chat;
9
10use App\Libraries\UserChannelList;
11use App\Models\Chat\Channel;
12use App\Models\Chat\Message;
13use App\Models\Multiplayer\ScoreLink;
14use App\Models\Multiplayer\UserScoreAggregate;
15use App\Models\User;
16use Illuminate\Testing\AssertableJsonString;
17use Illuminate\Testing\Fluent\AssertableJson;
18use Tests\TestCase;
19
20class ChannelsControllerTest extends TestCase
21{
22 private User $user;
23 private User $anotherUser;
24 private Channel $pmChannel;
25 private Channel $privateChannel;
26 private Channel $publicChannel;
27 private Message $publicMessage;
28
29 //region GET /chat/channels - Get Channel List
30 public function testChannelIndexWhenGuest()
31 {
32 $this->json('GET', route('api.chat.channels.index'))
33 ->assertStatus(401);
34 }
35
36 public function testChannelIndex()
37 {
38 $this->actAsScopedUser($this->user, ['*']);
39 $this->json('GET', route('api.chat.channels.index'))
40 ->assertStatus(200)
41 ->assertJsonFragment(['channel_id' => $this->publicChannel->channel_id])
42 ->assertJsonMissing(['channel_id' => $this->privateChannel->channel_id])
43 ->assertJsonMissing(['channel_id' => $this->pmChannel->channel_id]);
44 }
45
46 //endregion
47
48 //region POST /chat/channels - Create and join channel
49 public function testChannelStoreAnnouncement()
50 {
51 $sender = User::factory()->withGroup('announce')->create();
52 $users = User::factory()->count(2)->create();
53
54 $this->actAsScopedUser($sender, ['*']);
55 $this
56 ->json('POST', route('api.chat.channels.store'), [
57 'channel' => [
58 'description' => 'really',
59 'name' => 'important stuff',
60 ],
61 'message' => 'announcements!!!',
62 'target_ids' => $users->pluck('user_id')->toArray(),
63 'type' => Channel::TYPES['announce'],
64 ])
65 ->assertSuccessful()
66 ->assertJson(fn (AssertableJson $json) => $json
67 ->where('type', Channel::TYPES['announce'])
68 ->etc());
69 }
70
71 public function testChannelStoreInvalid()
72 {
73 $this->actAsScopedUser($this->user, ['*']);
74 $this->json('POST', route('api.chat.channels.store'), [
75 'type' => Channel::TYPES['public'],
76 ])->assertStatus(422);
77
78 $this->json('POST', route('api.chat.channels.store'), [
79 'type' => Channel::TYPES['pm'],
80 ])->assertStatus(422);
81 }
82
83 public function testChannelStorePM()
84 {
85 $initialChannels = Channel::count();
86
87 $this->actAsScopedUser($this->user, ['*']);
88 $this->json('POST', route('api.chat.channels.store'), [
89 'target_id' => $this->anotherUser->getKey(),
90 'type' => Channel::TYPES['pm'],
91 ])->assertSuccessful()
92 ->assertJsonFragment([
93 'channel_id' => null,
94 'recent_messages' => [],
95 ]);
96
97 $this->assertSame($initialChannels, Channel::count());
98 }
99
100 public function testChannelStorePMUserLeft()
101 {
102 $channel = Channel::createPM($this->user, $this->anotherUser);
103 $channel->removeUser($this->user);
104
105 // sanity check
106 $this->getAssertableChannelList($this->user)
107 ->assertMissing(['channel_id' => $channel->getKey()]);
108
109 $this->actAsScopedUser($this->user, ['*']);
110 $this->json('POST', route('api.chat.channels.store'), [
111 'target_id' => $this->anotherUser->getKey(),
112 'type' => Channel::TYPES['pm'],
113 ])->assertSuccessful()
114 ->assertJsonFragment([
115 'channel_id' => $channel->getKey(),
116 'recent_messages' => [],
117 'type' => Channel::TYPES['pm'],
118 ]);
119
120 $this->assertTrue($channel->hasUser($this->user));
121 }
122
123 //endregion
124
125 /**
126 * @dataProvider dataProvider
127 */
128 public function testChannelJoin($type, $success)
129 {
130 $channel = Channel::factory()->type($type)->create();
131 $status = $success ? 200 : 403;
132
133 $this->actAsScopedUser($this->user, ['*']);
134
135 $this->getAssertableChannelList($this->user)
136 ->assertMissing(['channel_id' => $channel->getKey()]);
137
138 // join channel
139 $request = $this->json('PUT', route('api.chat.channels.join', [
140 'channel' => $channel->getKey(),
141 'user' => $this->user->getKey(),
142 ]))->assertStatus($status);
143
144 if ($success) {
145 $request->assertJsonFragment(['channel_id' => $channel->getKey()]);
146
147 // ensure now in channel
148 $this->getAssertableChannelList($this->user)
149 ->assertFragment(['channel_id' => $channel->getKey()]);
150 }
151 }
152
153 //region PUT /chat/channels/[channel_id]/users/[user_id] - Join Channel (public)
154 public function testChannelJoinPublicWhenGuest() // fail
155 {
156 $this->json('PUT', route('api.chat.channels.join', [
157 'channel' => $this->publicChannel->channel_id,
158 'user' => $this->user->user_id,
159 ]))
160 ->assertStatus(401);
161 }
162
163 public function testChannelJoinPublicWhenDifferentUser() // fail
164 {
165 $this->actAsScopedUser($this->user, ['*']);
166 $this->json('PUT', route('api.chat.channels.join', [
167 'channel' => $this->publicChannel->channel_id,
168 'user' => $this->anotherUser->user_id,
169 ]))
170 ->assertStatus(403);
171 }
172
173 public function testChannelJoinPM() // fail
174 {
175 $this->actAsScopedUser($this->user, ['*']);
176 $this->json('PUT', route('api.chat.channels.join', [
177 'channel' => $this->pmChannel->channel_id,
178 'user' => $this->user->user_id,
179 ]))
180 ->assertStatus(403);
181 }
182
183 public function testChannelJoinMultiplayerWhenNotParticipated()
184 {
185 $scoreLink = ScoreLink::factory()->create();
186 UserScoreAggregate::lookupOrDefault($scoreLink->user, $scoreLink->playlistItem->room)->recalculate();
187
188 $this->actAsScopedUser($this->user, ['*']);
189 $request = $this->json('PUT', route('api.chat.channels.join', [
190 'channel' => $scoreLink->playlistItem->room->channel_id,
191 'user' => $this->user->getKey(),
192 ]));
193
194 $request->assertStatus(403);
195 }
196
197 public function testChannelJoinMultiplayerWhenParticipated()
198 {
199 $scoreLink = ScoreLink::factory()->create(['user_id' => $this->user]);
200 UserScoreAggregate::lookupOrDefault($scoreLink->user, $scoreLink->playlistItem->room)->recalculate();
201
202 $this->actAsScopedUser($this->user, ['*']);
203 $request = $this->json('PUT', route('api.chat.channels.join', [
204 'channel' => $scoreLink->playlistItem->room->channel_id,
205 'user' => $this->user->getKey(),
206 ]));
207
208 $request->assertStatus(403);
209 }
210
211 //endregion
212
213 //region PUT /chat/channels/[channel_id]/mark-as-read/[message_id] - Mark Channel as Read
214 public function testChannelMarkAsReadWhenGuest() // fail
215 {
216 $this->json(
217 'PUT',
218 route('api.chat.channels.mark-as-read', [
219 'channel' => $this->publicChannel->channel_id,
220 'message' => $this->publicMessage->message_id,
221 ])
222 )
223 ->assertStatus(401);
224 }
225
226 public function testChannelMarkAsReadWhenUnjoined() // fail
227 {
228 $this->actAsScopedUser($this->user, ['*']);
229 $this->json(
230 'PUT',
231 route('api.chat.channels.mark-as-read', [
232 'channel' => $this->publicChannel->channel_id,
233 'message' => $this->publicMessage->message_id,
234 ])
235 )
236 ->assertStatus(404);
237 }
238
239 public function testChannelMarkAsReadWhenJoined() // success
240 {
241 $this->actAsScopedUser($this->user, ['*']);
242 $this->json('PUT', route('api.chat.channels.join', [
243 'channel' => $this->publicChannel->channel_id,
244 'user' => $this->user->user_id,
245 ]));
246
247 $this->actAsScopedUser($this->user, ['*']);
248 $this->json(
249 'PUT',
250 route('api.chat.channels.mark-as-read', [
251 'channel' => $this->publicChannel->channel_id,
252 'message' => $this->publicMessage->message_id,
253 ])
254 )
255 ->assertStatus(204);
256
257 $this->getAssertableChannelList($this->user)
258 ->assertPath('0.current_user_attributes.last_read_id', $this->publicMessage->message_id)
259 ->assertFragment([
260 'channel_id' => $this->publicChannel->channel_id,
261 'last_read_id' => $this->publicMessage->message_id,
262 ]);
263 }
264
265 public function testChannelMarkAsReadBackwards() // success (with no change)
266 {
267 $newerPublicMessage = Message::factory()->create(['channel_id' => $this->publicChannel]);
268
269 $this->actAsScopedUser($this->user, ['*']);
270 $this->json('PUT', route('api.chat.channels.join', [
271 'channel' => $this->publicChannel->channel_id,
272 'user' => $this->user->user_id,
273 ]));
274
275 // mark as read to $newerPublicMessage->message_id
276 $this->actAsScopedUser($this->user, ['*']);
277 $this->json(
278 'PUT',
279 route('api.chat.channels.mark-as-read', [
280 'channel' => $this->publicChannel->channel_id,
281 'message' => $newerPublicMessage->message_id,
282 ])
283 )
284 ->assertStatus(204);
285
286 $this->getAssertableChannelList($this->user)
287 ->assertPath('0.current_user_attributes.last_read_id', $newerPublicMessage->message_id)
288 ->assertFragment([
289 'channel_id' => $this->publicChannel->channel_id,
290 'last_read_id' => $newerPublicMessage->message_id,
291 ]);
292
293 // attempt to mark as read to the older $this->publicMessage->message_id
294 $this->actAsScopedUser($this->user, ['*']);
295 $this->json(
296 'PUT',
297 route('api.chat.channels.mark-as-read', [
298 'channel' => $this->publicChannel->channel_id,
299 'message' => $this->publicMessage->message_id,
300 ])
301 )
302 ->assertStatus(204);
303
304 $this->getAssertableChannelList($this->user)
305 ->assertPath('0.current_user_attributes.last_read_id', $newerPublicMessage->message_id)
306 ->assertFragment([
307 'channel_id' => $this->publicChannel->channel_id,
308 'last_read_id' => $newerPublicMessage->message_id,
309 ]);
310 }
311
312 //endregion
313
314 //region DELETE /chat/channels/[channel_id]/users/[user_id] - Leave Channel
315 /**
316 * @dataProvider dataProvider
317 */
318 public function testChannelLeave($type, $success)
319 {
320 $channel = Channel::factory()->type($type)->create();
321 $channel->addUser($this->user);
322 $status = $success ? 204 : 403;
323
324 $this->actAsScopedUser($this->user, ['*']);
325
326 // ensure in channel
327 $this->getAssertableChannelList($this->user)
328 ->assertFragment(['channel_id' => $channel->getKey()]);
329
330 // leave channel
331 $this->json('DELETE', route('api.chat.channels.part', [
332 'channel' => $channel->channel_id,
333 'user' => $this->user->getKey(),
334 ]))
335 ->assertStatus($status);
336
337 $channelList = $this->getAssertableChannelList($this->user);
338
339 if ($success) {
340 // ensure no longer in channel
341 $channelList->assertMissing(['channel_id' => $channel->getKey()]);
342 } else {
343 // ensure still in channel
344 $channelList->assertFragment(['channel_id' => $channel->getKey()]);
345 }
346 }
347
348 /**
349 * @dataProvider dataProvider
350 */
351 public function testChannelLeaveWhenNotJoined($type, $success)
352 {
353 $channel = Channel::factory()->type($type)->create();
354 $status = $success ? 204 : 403;
355
356 $this->actAsScopedUser($this->user, ['*']);
357
358 // ensure not in channel
359 $this->getAssertableChannelList($this->user)
360 ->assertMissing(['channel_id' => $channel->getKey()]);
361
362 // leave channel
363 $this->json('DELETE', route('api.chat.channels.part', [
364 'channel' => $channel->channel_id,
365 'user' => $this->user->getKey(),
366 ]))
367 ->assertStatus($status);
368 }
369
370 public function testChannelLeavePublicWhenGuest() // fail
371 {
372 $this->json('DELETE', route('api.chat.channels.part', [
373 'channel' => $this->publicChannel->channel_id,
374 'user' => $this->user->user_id,
375 ]))
376 ->assertStatus(401);
377 }
378
379 //endregion
380
381 public static function dataProvider()
382 {
383 return [
384 ['private', false],
385 ['public', true],
386 ['tourney', true],
387 ];
388 }
389
390 protected function setUp(): void
391 {
392 parent::setUp();
393
394 $this->user = User::factory()->create();
395 $this->anotherUser = User::factory()->create();
396 $this->publicChannel = Channel::factory()->type('public')->create();
397 $this->privateChannel = Channel::factory()->type('private')->create();
398 $this->pmChannel = Channel::factory()->type('pm')->create();
399 $this->publicMessage = Message::factory()->create(['channel_id' => $this->publicChannel]);
400 }
401
402 private function getAssertableChannelList(User $user): AssertableJsonString
403 {
404 return new AssertableJsonString((new UserChannelList($user))->get());
405 }
406}