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\Models\Multiplayer;
7
8use App\Exceptions\InvariantException;
9use App\Models\Beatmap;
10use App\Models\ChatFilter;
11use App\Models\Multiplayer\PlaylistItem;
12use App\Models\Multiplayer\Room;
13use App\Models\User;
14use Exception;
15use Tests\TestCase;
16
17class RoomTest extends TestCase
18{
19 /**
20 * @dataProvider startGameDurationDataProvider
21 */
22 public function testStartGameDuration(int $duration, bool $isSupporter, bool $expectException)
23 {
24 $beatmap = Beatmap::factory()->create();
25 $user = User::factory();
26 if ($isSupporter) {
27 $user = $user->supporter();
28 }
29
30 $user = $user->create();
31
32 $params = [
33 'duration' => $duration,
34 'name' => 'test',
35 'playlist' => [
36 [
37 'beatmap_id' => $beatmap->getKey(),
38 'ruleset_id' => $beatmap->playmode,
39 ],
40 ],
41 ];
42
43 if ($expectException) {
44 $this->expectException(InvariantException::class);
45 $this->expectExceptionMessage(osu_trans('multiplayer.room.errors.duration_too_long'));
46 $this->expectCountChange(fn () => Room::count(), 0);
47 } else {
48 $this->expectCountChange(fn () => Room::count(), 1);
49 }
50
51 $room = (new Room())->startGame($user, $params);
52 $this->assertTrue($room->exists);
53 }
54
55 public function testStartGameWithBeatmap()
56 {
57 $beatmap = Beatmap::factory()->create();
58 $user = User::factory()->create();
59
60 $params = [
61 'duration' => 60,
62 'name' => 'test',
63 'playlist' => [
64 [
65 'beatmap_id' => $beatmap->getKey(),
66 'ruleset_id' => $beatmap->playmode,
67 ],
68 ],
69 ];
70
71 $room = (new Room())->startGame($user, $params);
72 $this->assertTrue($room->exists);
73 }
74
75 public function testStartGameWithDeletedBeatmap()
76 {
77 $beatmap = Beatmap::factory()->create(['deleted_at' => now()]);
78 $user = User::factory()->create();
79
80 $params = [
81 'duration' => 60,
82 'name' => 'test',
83 'playlist' => [
84 [
85 'beatmap_id' => $beatmap->getKey(),
86 'ruleset_id' => $beatmap->playmode,
87 ],
88 ],
89 ];
90
91 $this->expectException(InvariantException::class);
92 (new Room())->startGame($user, $params);
93 }
94
95 public function testStartGameWithInvalidRuleset()
96 {
97 $beatmap = Beatmap::factory()->create([
98 'playmode' => 2,
99 ]);
100 $user = User::factory()->create();
101
102 $params = [
103 'duration' => 60,
104 'name' => 'test',
105 'playlist' => [
106 [
107 'beatmap_id' => $beatmap->getKey(),
108 'ruleset_id' => 0,
109 ],
110 ],
111 ];
112
113 $this->expectException(InvariantException::class);
114 $this->expectExceptionMessageMatches('/^invalid ruleset_id for beatmap \d+$/');
115 (new Room())->startGame($user, $params);
116 }
117
118 public function testRoomHasEnded()
119 {
120 $user = User::factory()->create();
121 $room = Room::factory()->ended()->create();
122 $playlistItem = PlaylistItem::factory()->create([
123 'room_id' => $room,
124 ]);
125
126 $this->expectException(InvariantException::class);
127 static::roomStartPlay($user, $playlistItem);
128 }
129
130 public function testStartPlay(): void
131 {
132 $user = User::factory()->create();
133 $room = Room::factory()->create();
134 $playlistItem = PlaylistItem::factory()->create(['room_id' => $room]);
135
136 $this->expectCountChange(fn () => $room->participant_count, 1);
137 $this->expectCountChange(fn () => $room->userHighScores()->count(), 1);
138 $this->expectCountChange(fn () => $playlistItem->scoreTokens()->count(), 1);
139
140 static::roomStartPlay($user, $playlistItem);
141 $room->refresh();
142
143 $this->assertSame($user->getKey(), $playlistItem->scoreTokens()->last()->user_id);
144 }
145
146 public function testMaxAttemptsReached()
147 {
148 $user = User::factory()->create();
149 $room = Room::factory()->create(['max_attempts' => 2]);
150 $playlistItem1 = PlaylistItem::factory()->create(['room_id' => $room]);
151 $playlistItem2 = PlaylistItem::factory()->create(['room_id' => $room]);
152
153 static::roomStartPlay($user, $playlistItem1);
154 $this->assertTrue(true);
155
156 static::roomStartPlay($user, $playlistItem2);
157 $this->assertTrue(true);
158
159 $this->expectException(InvariantException::class);
160 static::roomStartPlay($user, $playlistItem1);
161 }
162
163 public function testMaxAttemptsForItemReached()
164 {
165 $user = User::factory()->create();
166 $room = Room::factory()->create();
167 $playlistItem1 = PlaylistItem::factory()->create([
168 'room_id' => $room,
169 'max_attempts' => 1,
170 ]);
171 $playlistItem2 = PlaylistItem::factory()->create([
172 'room_id' => $room,
173 'max_attempts' => 1,
174 ]);
175
176 $initialCount = $playlistItem1->scoreTokens()->count();
177 static::roomStartPlay($user, $playlistItem1);
178 $this->assertSame($initialCount + 1, $playlistItem1->scoreTokens()->count());
179
180 $initialCount = $playlistItem1->scoreTokens()->count();
181 try {
182 static::roomStartPlay($user, $playlistItem1);
183 } catch (Exception $ex) {
184 $this->assertTrue($ex instanceof InvariantException);
185 }
186 $this->assertSame($initialCount, $playlistItem1->scoreTokens()->count());
187
188 $initialCount = $playlistItem2->scoreTokens()->count();
189 static::roomStartPlay($user, $playlistItem2);
190 $this->assertSame($initialCount + 1, $playlistItem2->scoreTokens()->count());
191 }
192
193 public function testCannotStartPlayedItem()
194 {
195 $beatmap = Beatmap::factory()->create();
196 $user = User::factory()->create();
197
198 $params = [
199 'name' => 'test',
200 'playlist' => [
201 [
202 'beatmap_id' => $beatmap->getKey(),
203 'ruleset_id' => $beatmap->playmode,
204 'played_at' => time(),
205 ],
206 ],
207 ];
208
209 $this->expectException(InvariantException::class);
210 (new Room())->startGame($user, $params);
211 }
212
213 public function testNameFiltering()
214 {
215 ChatFilter::factory()->create([
216 'match' => 'bad',
217 'replacement' => 'good',
218 ]);
219 $beatmap = Beatmap::factory()->create();
220 $user = User::factory()->create();
221
222 $params = [
223 'name' => 'bad word',
224 'playlist' => [
225 [
226 'beatmap_id' => $beatmap->getKey(),
227 'ruleset_id' => $beatmap->playmode,
228 'played_at' => time(),
229 ],
230 ],
231 'type' => 'head_to_head',
232 ];
233
234 $room = new Room();
235 $room->startGame($user, $params);
236 $this->assertSame('good word', $room->name);
237 }
238
239 public static function startGameDurationDataProvider()
240 {
241 static $dayMinutes = 1440;
242 static::createApp();
243
244 $maxDuration = $GLOBALS['cfg']['osu']['user']['max_multiplayer_duration'];
245 $maxDurationSupporter = $GLOBALS['cfg']['osu']['user']['max_multiplayer_duration_supporter'];
246
247 return [
248 '2 weeks' => [$dayMinutes * $maxDuration, false, false],
249 '2 weeks (with supporter)' => [$dayMinutes * $maxDuration, true, false],
250 'more than 2 weeks' => [$dayMinutes * $maxDuration + 1, false, true],
251 'more than 2 weeks (with supporter)' => [$dayMinutes * $maxDuration + 1, true, false],
252 '3 months' => [$dayMinutes * $maxDurationSupporter, false, true],
253 '3 months (with supporter)' => [$dayMinutes * $maxDurationSupporter, true, false],
254 'more than 3 months' => [$dayMinutes * $maxDurationSupporter + 1, false, true],
255 'more than 3 months (with supporter)' => [$dayMinutes * $maxDurationSupporter + 1, true, true],
256 ];
257 }
258}