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\Libraries\Beatmapset;
9
10use App\Exceptions\AuthorizationException;
11use App\Exceptions\InvariantException;
12use App\Jobs\Notifications\BeatmapOwnerChange;
13use App\Libraries\Beatmapset\ChangeBeatmapOwners;
14use App\Models\Beatmap;
15use App\Models\BeatmapOwner;
16use App\Models\Beatmapset;
17use App\Models\BeatmapsetEvent;
18use App\Models\DeletedUser;
19use App\Models\User;
20use Arr;
21use Bus;
22use Tests\TestCase;
23
24class ChangeBeatmapOwnersTest extends TestCase
25{
26 public static function dataProviderForInvalidUser(): array
27 {
28 return [
29 ['bot'],
30 ['no_profile'],
31 ];
32 }
33
34 public static function dataProviderForUpdateOwner(): array
35 {
36 return [
37 'existing restricted user' => [['restricted', 'default'], true],
38 'new restricted user' => [['default', 'restricted'], false],
39 'new user' => [['default', 'default'], true],
40 ];
41 }
42
43 public static function dataProviderForUpdateOwnerLoved(): array
44 {
45 return [
46 [Beatmapset::STATES['graveyard'], true],
47 [Beatmapset::STATES['loved'], true],
48 [Beatmapset::STATES['ranked'], false],
49 [Beatmapset::STATES['wip'], false],
50 ];
51 }
52
53 public function testMissingUser(): void
54 {
55 $moderator = User::factory()->withGroup('nat')->create();
56 $otherUser = User::factory()->create();
57 $missingUserId = User::max('user_id') + 1;
58 $userIds = [$missingUserId, $otherUser->getKey()];
59
60 $beatmap = Beatmap::factory()
61 ->state(['user_id' => $missingUserId])
62 ->has(BeatmapOwner::factory()->state(['user_id' => $missingUserId]))
63 ->for(Beatmapset::factory()->pending()->state(['user_id' => $missingUserId]))
64 ->create();
65
66 $this->expectCountChange(fn () => BeatmapsetEvent::count(), 1);
67
68 (new ChangeBeatmapOwners($beatmap, $userIds, $moderator))->handle();
69
70 $beatmap = $beatmap->fresh();
71 $newOwners = $beatmap->getOwners();
72 $this->assertEqualsCanonicalizing($userIds, $newOwners->pluck('user_id')->toArray());
73 $this->assertTrue($newOwners->find($missingUserId) instanceof DeletedUser);
74 $this->assertSame($userIds[0], $beatmap->user_id);
75
76 Bus::assertDispatched(BeatmapOwnerChange::class);
77 }
78
79 /**
80 * @dataProvider dataProviderForInvalidUser
81 */
82 public function testInvalidUser(string $group): void
83 {
84 $moderator = User::factory()->withGroup('nat')->create();
85 $owner = User::factory()->create();
86 $invalidUser = User::factory()->withGroup($group)->create();
87
88 $beatmap = Beatmap::factory()
89 ->for(Beatmapset::factory()->pending()->owner($owner))
90 ->owner($owner)
91 ->create();
92
93 $this->expectCountChange(fn () => BeatmapsetEvent::count(), 0);
94
95 $this->expectExceptionCallable(
96 fn () => (new ChangeBeatmapOwners($beatmap, [$invalidUser->getKey()], $moderator))->handle(),
97 InvariantException::class,
98 );
99
100 $beatmap = $beatmap->fresh();
101 $this->assertEqualsCanonicalizing([$owner->getKey()], $beatmap->getOwners()->pluck('user_id')->toArray());
102
103 Bus::assertNotDispatched(BeatmapOwnerChange::class);
104 }
105
106 /**
107 * @dataProvider dataProviderForUpdateOwner
108 */
109 public function testUpdateOwner(array $states, bool $success): void
110 {
111 $factory = User::factory();
112 $moderator = $factory->withGroup('nat')->create();
113 $users = array_map(fn ($state) => $factory->$state()->create(), $states);
114 $owner = $users[0];
115
116 $beatmap = Beatmap::factory()
117 ->for(Beatmapset::factory()->pending()->owner($owner))
118 ->owner($owner)
119 ->create();
120
121 $this->expectCountChange(fn () => BeatmapsetEvent::count(), $success ? 1 : 0);
122
123 $this->expectExceptionCallable(
124 fn () => (new ChangeBeatmapOwners($beatmap, Arr::pluck($users, 'user_id'), $moderator))->handle(),
125 $success ? null : InvariantException::class,
126 );
127
128 $beatmap = $beatmap->fresh();
129
130 if ($success) {
131 $this->assertEqualsCanonicalizing(Arr::pluck($users, 'user_id'), $beatmap->getOwners()->pluck('user_id')->toArray());
132 $this->assertSame($users[0]->getKey(), $beatmap->user_id);
133 Bus::assertDispatched(BeatmapOwnerChange::class);
134 } else {
135 $this->assertEqualsCanonicalizing([$owner->getKey()], $beatmap->getOwners()->pluck('user_id')->toArray());
136 Bus::assertNotDispatched(BeatmapOwnerChange::class);
137 }
138 }
139
140 public function testUpdateOwnerExistingRestrictedUser(): void
141 {
142 $source = User::factory()->withGroup('gmt')->create();
143 $owner = User::factory()->restricted()->create();
144 $users = [User::factory()->create(), $owner];
145 $ownerId = $owner->getKey();
146
147 $beatmap = Beatmap::factory()
148 ->for(Beatmapset::factory()->pending()->owner($owner))
149 ->owner($owner)
150 ->create();
151
152 $this->expectCountChange(fn () => BeatmapsetEvent::count(), 1);
153
154 (new ChangeBeatmapOwners($beatmap, Arr::pluck($users, 'user_id'), $source))->handle();
155
156 $beatmap = $beatmap->fresh();
157 $newOwners = $beatmap->getOwners();
158 $this->assertCount(count($users), $newOwners);
159 $this->assertEqualsCanonicalizing(Arr::pluck($users, 'user_id'), $newOwners->pluck('user_id')->toArray());
160
161 Bus::assertDispatched(BeatmapOwnerChange::class);
162 }
163
164 public function testUpdateOwnerInvalidState(): void
165 {
166 $user = User::factory()->create();
167 $owner = User::factory()->create();
168 $beatmap = Beatmap::factory()
169 ->for(Beatmapset::factory()->qualified()->owner($owner))
170 ->owner($owner)
171 ->create();
172
173 $this->expectCountChange(fn () => BeatmapsetEvent::count(), 0);
174 $this->expectExceptionCallable(
175 fn () => (new ChangeBeatmapOwners($beatmap, [$user->getKey()], $owner))->handle(),
176 AuthorizationException::class
177 );
178
179 $beatmap = $beatmap->fresh();
180 $this->assertEqualsCanonicalizing([$owner->getKey()], $beatmap->getOwners()->pluck('user_id')->toArray());
181 $this->assertSame($owner->getKey(), $beatmap->user_id);
182
183 Bus::assertNotDispatched(BeatmapOwnerChange::class);
184 }
185
186 public function testUpdateOwnerInvalidUser(): void
187 {
188 $owner = User::factory()->create();
189 $beatmap = Beatmap::factory()
190 ->for(Beatmapset::factory()->pending()->owner($owner))
191 ->owner($owner)
192 ->create();
193
194 $this->expectCountChange(fn () => BeatmapsetEvent::count(), 0);
195 $this->expectExceptionCallable(
196 fn () => (new ChangeBeatmapOwners($beatmap, [User::max('user_id') + 1], $owner))->handle(),
197 InvariantException::class
198 );
199
200 $beatmap = $beatmap->fresh();
201 $this->assertEqualsCanonicalizing([$owner->getKey()], $beatmap->getOwners()->pluck('user_id')->toArray());
202 $this->assertSame($owner->getKey(), $beatmap->user_id);
203
204 Bus::assertNotDispatched(BeatmapOwnerChange::class);
205 }
206
207 /**
208 * @dataProvider dataProviderForUpdateOwnerLoved
209 */
210 public function testUpdateOwnerLoved(int $approved, bool $ok): void
211 {
212 $moderator = User::factory()->withGroup('loved')->create();
213 $user = User::factory()->create();
214 $owner = User::factory()->create();
215 $beatmap = Beatmap::factory()
216 ->for(Beatmapset::factory()->state([
217 'approved' => $approved,
218 'approved_date' => now(),
219 ])->owner($owner))
220 ->owner($owner)
221 ->create();
222
223 $this->expectCountChange(fn () => BeatmapsetEvent::count(), $ok ? 1 : 0);
224
225 $this->expectExceptionCallable(
226 fn () => (new ChangeBeatmapOwners($beatmap, [$user->getKey()], $moderator))->handle(),
227 $ok ? null : AuthorizationException::class,
228 );
229
230 $beatmap = $beatmap->fresh();
231 $expectedUser = $ok ? $user : $owner;
232 $this->assertEqualsCanonicalizing([$expectedUser->getKey()], $beatmap->getOwners()->pluck('user_id')->toArray());
233 $this->assertSame($expectedUser->getKey(), $beatmap->user_id);
234
235 if ($ok) {
236 Bus::assertDispatched(BeatmapOwnerChange::class);
237 } else {
238 Bus::assertNotDispatched(BeatmapOwnerChange::class);
239 }
240 }
241
242 public function testUpdateOwnerModerator(): void
243 {
244 $moderator = User::factory()->withGroup('nat')->create();
245 $user = User::factory()->create();
246 $owner = User::factory()->create();
247 $beatmap = Beatmap::factory()
248 ->for(Beatmapset::factory()->state([
249 'approved' => Beatmapset::STATES['ranked'],
250 'approved_date' => now(),
251 ])->owner($owner))
252 ->owner($owner)
253 ->create();
254
255 $this->expectCountChange(fn () => BeatmapsetEvent::count(), 1);
256
257 (new ChangeBeatmapOwners($beatmap, [$user->getKey()], $moderator))->handle();
258
259 $beatmap = $beatmap->fresh();
260 $this->assertEqualsCanonicalizing([$user->getKey()], $beatmap->getOwners()->pluck('user_id')->toArray());
261 $this->assertSame($user->getKey(), $beatmap->user_id);
262 }
263
264 public function testUpdateOwnerNotOwner(): void
265 {
266 $user = User::factory()->create();
267 $owner = User::factory()->create();
268 $beatmap = Beatmap::factory()
269 ->for(Beatmapset::factory()->state([
270 'approved' => Beatmapset::STATES['ranked'],
271 'approved_date' => now(),
272 ])->owner($owner))
273 ->owner($owner)
274 ->create();
275
276 $this->expectCountChange(fn () => BeatmapsetEvent::count(), 0);
277 $this->expectExceptionCallable(
278 fn () => (new ChangeBeatmapOwners($beatmap, [$user->getKey()], $user))->handle(),
279 AuthorizationException::class,
280 );
281
282 $beatmap = $beatmap->fresh();
283 $this->assertEqualsCanonicalizing([$owner->getKey()], $beatmap->getOwners()->pluck('user_id')->toArray());
284 $this->assertSame($owner->getKey(), $beatmap->user_id);
285
286 Bus::assertNotDispatched(BeatmapOwnerChange::class);
287 }
288
289 public function testUpdateOwnerSameOwner(): void
290 {
291 $owner = User::factory()->create();
292 $beatmap = Beatmap::factory()
293 ->for(Beatmapset::factory()->pending()->owner($owner))
294 ->owner($owner)
295 ->create();
296
297 $this->expectCountChange(fn () => BeatmapsetEvent::count(), 0);
298 (new ChangeBeatmapOwners($beatmap, [$owner->getKey()], $owner))->handle();
299
300 $beatmap = $beatmap->fresh();
301 $this->assertEqualsCanonicalizing([$owner->getKey()], $beatmap->getOwners()->pluck('user_id')->toArray());
302 $this->assertSame($owner->getKey(), $beatmap->user_id);
303
304 Bus::assertNotDispatched(BeatmapOwnerChange::class);
305 }
306
307 protected function setUp(): void
308 {
309 parent::setUp();
310
311 Bus::fake([BeatmapOwnerChange::class]);
312 }
313}