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;
9
10use App\Models\Beatmap;
11use App\Models\ScorePin;
12use App\Models\Solo\Score;
13use App\Models\User;
14use Tests\TestCase;
15
16class ScorePinsControllerTest extends TestCase
17{
18 private static function createScore(?User $user = null, ?int $rulesetId = null, ?bool $passed = null): Score
19 {
20 if ($rulesetId !== null) {
21 $params['ruleset_id'] = $rulesetId;
22 }
23 if ($user !== null) {
24 $params['user_id'] = $user;
25 }
26 $params['passed'] = $passed ?? true;
27
28 return Score::factory()->create($params);
29 }
30
31 private static function makeParams(Score $score): array
32 {
33 return [
34 'score_id' => $score->getKey(),
35 ];
36 }
37
38 public function testDestroy()
39 {
40 $pin = ScorePin::factory()->withScore(static::createScore())->create();
41
42 $this->expectCountChange(fn () => ScorePin::count(), -1);
43
44 $this->actAsUser($pin->user, true);
45
46 $this
47 ->delete(route('score-pins.destroy', static::makeParams($pin->score)))
48 ->assertSuccessful();
49 }
50
51 public function testDestroyAsDifferentUser()
52 {
53 $pin = ScorePin::factory()->withScore(static::createScore())->create();
54 $otherUser = User::factory()->create();
55
56 $this->expectCountChange(fn () => ScorePin::count(), 0);
57
58 $this->actAsUser($otherUser, true);
59
60 $this
61 ->delete(route('score-pins.destroy', static::makeParams($pin->score)))
62 ->assertSuccessful();
63 }
64
65 public function testDestroyAsGuest()
66 {
67 $pin = ScorePin::factory()->withScore(static::createScore())->create();
68
69 $this->expectCountChange(fn () => ScorePin::count(), 0);
70
71 $this
72 ->delete(route('score-pins.destroy', static::makeParams($pin->score)))
73 ->assertStatus(401);
74 }
75
76 // moving: [0]. expected order: [1] < [0]
77 public function testReorderMoveBottom()
78 {
79 $user = User::factory()->create();
80 $pins = collect([0, 1])->map(fn ($order) => ScorePin
81 ::factory(['display_order' => $order])
82 ->withScore(static::createScore($user, Beatmap::MODES['osu']))
83 ->create());
84
85 $this->actAsUser($user, true);
86 $this
87 ->put(route('score-pins.reorder'), [
88 ...static::makeParams($pins[0]->score),
89 'order1' => static::makeParams($pins[1]->score),
90 ])->assertSuccessful();
91
92 $pins->map->refresh();
93 $this->assertTrue($pins[1]->display_order < $pins[0]->display_order);
94 }
95
96 // moving: [0]. expected order: [1] < [0] < [2]
97 public function testReorderMoveDown()
98 {
99 $user = User::factory()->create();
100 $pins = collect([0, 1, 2])->map(fn ($order) => ScorePin
101 ::factory(['display_order' => $order])
102 ->withScore(static::createScore($user, Beatmap::MODES['osu']))
103 ->create());
104
105 $this->actAsUser($user, true);
106 $this
107 ->put(route('score-pins.reorder'), [
108 ...static::makeParams($pins[0]->score),
109 'order1' => static::makeParams($pins[1]->score),
110 ])->assertSuccessful();
111
112 $pins->map->refresh();
113 $this->assertTrue($pins[1]->display_order < $pins[0]->display_order);
114 $this->assertTrue($pins[0]->display_order < $pins[2]->display_order);
115 }
116
117 // moving: [1]. expected order: [1] < [0]
118 public function testReorderMoveTop()
119 {
120 $user = User::factory()->create();
121 $pins = collect([0, 1])->map(fn ($order) => ScorePin
122 ::factory(['display_order' => $order])
123 ->withScore(static::createScore($user, Beatmap::MODES['osu']))
124 ->create());
125
126 $this->actAsUser($user, true);
127 $this
128 ->put(route('score-pins.reorder'), [
129 ...static::makeParams($pins[1]->score),
130 'order3' => static::makeParams($pins[0]->score),
131 ])->assertSuccessful();
132
133 $pins->map->refresh();
134 $this->assertTrue($pins[1]->display_order < $pins[0]->display_order);
135 }
136
137 // moving: [2]. expected order: [0] < [2] < [1]
138 public function testReorderMoveUp()
139 {
140 $user = User::factory()->create();
141 $pins = collect([0, 1, 2])->map(fn ($order) => ScorePin
142 ::factory(['display_order' => $order])
143 ->withScore(static::createScore($user, Beatmap::MODES['osu']))
144 ->create());
145
146 $this->actAsUser($user, true);
147 $this
148 ->put(route('score-pins.reorder'), [
149 ...static::makeParams($pins[2]->score),
150 'order1' => static::makeParams($pins[0]->score),
151 ])->assertSuccessful();
152
153 $pins->map->refresh();
154 $this->assertTrue($pins[0]->display_order < $pins[2]->display_order);
155 $this->assertTrue($pins[2]->display_order < $pins[1]->display_order);
156 }
157
158 public function testStore()
159 {
160 $score = static::createScore();
161
162 $this->expectCountChange(fn () => $score->user->scorePins()->count(), 1);
163
164 $this->actAsUser($score->user, true);
165
166 $this
167 ->post(route('score-pins.store'), static::makeParams($score))
168 ->assertSuccessful();
169 }
170
171 public function testStoreAsGuest()
172 {
173 $score = static::createScore();
174
175 $this->expectCountChange(fn () => ScorePin::count(), 0);
176
177 $this
178 ->post(route('score-pins.store'), static::makeParams($score))
179 ->assertStatus(401);
180 }
181
182 public function testStoreAsNonOwner()
183 {
184 $score = static::createScore();
185 $otherUser = User::factory()->create();
186
187 $this->expectCountChange(fn () => ScorePin::count(), 0);
188
189 $this->actAsUser($otherUser, true);
190
191 $this
192 ->post(route('score-pins.store'), static::makeParams($score))
193 ->assertStatus(403);
194 }
195
196 // new score pin should always be above existing ones
197 public function testStoreDisplayOrder()
198 {
199 $user = User::factory()->create([
200 'osu_subscriber' => false,
201 ]);
202 $score1 = static::createScore($user, Beatmap::MODES['osu']);
203 $score2 = static::createScore($user, Beatmap::MODES['osu']);
204 $pin1 = ScorePin::factory()->withScore($score1)->create();
205
206 $this->actAsUser($user, true);
207
208 $this
209 ->post(route('score-pins.store'), static::makeParams($score2))
210 ->assertSuccessful();
211
212 $pin2 = $user->scorePins()->find($score2->getKey());
213 $this->assertTrue($pin1->display_order > $pin2->display_order);
214 }
215
216 public function testStoreDuplicate()
217 {
218 $score = static::createScore();
219 $pin = ScorePin::factory()->withScore($score)->create();
220
221 $this->expectCountChange(fn () => ScorePin::count(), 0);
222
223 $this->actAsUser($score->user, true);
224
225 $this
226 ->post(route('score-pins.store'), static::makeParams($score))
227 ->assertSuccessful();
228 }
229
230 public function testStoreInvalidScoreId()
231 {
232 Score::whereKey(1)->delete();
233
234 $this->expectCountChange(fn () => ScorePin::count(), 0);
235
236 $this->actAsUser(User::factory()->create(), true);
237
238 $this
239 ->post(route('score-pins.store'), ['score_id' => 1])
240 ->assertStatus(422);
241 }
242
243 public function testStoreLimit()
244 {
245 config_set('osu.user.max_score_pins', 1);
246
247 $user = User::factory()->create([
248 'osu_subscriber' => false,
249 ]);
250 $score1 = static::createScore($user, Beatmap::MODES['osu']);
251 $score2 = static::createScore($user, Beatmap::MODES['osu']);
252 $pin1 = ScorePin::factory()->withScore($score1)->create();
253
254 $this->expectCountChange(fn () => ScorePin::count(), 0);
255
256 $this->actAsUser($user, true);
257
258 $this
259 ->post(route('score-pins.store'), static::makeParams($score2))
260 ->assertStatus(403);
261 }
262
263 public function testStoreLimitDifferentMode()
264 {
265 config_set('osu.user.max_score_pins', 1);
266
267 $user = User::factory()->create([
268 'osu_subscriber' => false,
269 ]);
270 $score1 = static::createScore($user, Beatmap::MODES['osu']);
271 $score2 = static::createScore($user, Beatmap::MODES['taiko']);
272 $pin1 = ScorePin::factory()->withScore($score1)->create();
273
274 $this->expectCountChange(fn () => ScorePin::count(), 1);
275
276 $this->actAsUser($user, true);
277
278 $this
279 ->post(route('score-pins.store'), static::makeParams($score2))
280 ->assertSuccessful();
281 }
282}