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\Beatmapset;
12use App\Models\User;
13use Illuminate\Testing\Fluent\AssertableJson;
14use Tests\TestCase;
15
16class BeatmapsControllerTest extends TestCase
17{
18 private $user;
19 private $beatmap;
20
21 /**
22 * @group RequiresBeatmapDifficultyLookupCache
23 */
24 public function testAttributes(): void
25 {
26 $beatmap = $this->createExistingOsuBeatmap();
27
28 $this->actAsScopedUser(User::factory()->create(), ['public']);
29
30 $this->post(route('api.beatmaps.attributes', ['beatmap' => $beatmap->getKey()]), [
31 'mods' => 1,
32 ])
33 ->assertSuccessful()
34 ->assertJson(fn (AssertableJson $json) =>
35 $json
36 ->has('attributes.star_rating')
37 ->has('attributes.max_combo')
38 ->etc());
39 }
40
41 public function testAttributesInvalidRuleset(): void
42 {
43 $beatmap = $this->createExistingOsuBeatmap();
44
45 $this->actAsScopedUser(User::factory()->create(), ['public']);
46
47 $this->post(route('api.beatmaps.attributes', ['beatmap' => $beatmap->getKey(), 'ruleset' => 'invalid']))
48 ->assertStatus(422);
49 }
50
51 public function testAttributesInvalidRulesetId(): void
52 {
53 $beatmap = $this->createExistingOsuBeatmap();
54
55 $this->actAsScopedUser(User::factory()->create(), ['public']);
56
57 $this->post(route('api.beatmaps.attributes', ['beatmap' => $beatmap->getKey(), 'ruleset_id' => 1000]))
58 ->assertStatus(422);
59 }
60
61 public function testAttributesInvalidConversion(): void
62 {
63 $beatmap = $this->createExistingFruitsBeatmap();
64
65 $this->actAsScopedUser(User::factory()->create(), ['public']);
66
67 $this->post(route('api.beatmaps.attributes', ['beatmap' => $beatmap->getKey(), 'ruleset' => 'mania']))
68 ->assertStatus(422);
69 }
70
71 public function testIndexForApi(): void
72 {
73 $beatmap = Beatmap::factory()->create();
74 $beatmapB = Beatmap::factory()->create();
75 $beatmapC = Beatmap::factory()->create();
76 $beatmapC->beatmapset->update(['active' => false]);
77
78 $this->actAsScopedUser(User::factory()->create(), ['*']);
79
80 $this
81 ->get(route('api.beatmaps.index', ['ids' => [$beatmap->getKey(), $beatmapB->getKey(), $beatmapC->getKey()]]))
82 ->assertSuccessful()
83 ->assertJson(fn (AssertableJson $json) =>
84 $json
85 ->where('beatmaps.0.id', $beatmap->getKey())
86 ->where('beatmaps.1.id', $beatmapB->getKey())
87 ->missing('beatmaps.2')
88 ->etc());
89 }
90
91 public function testIndexForApiMissingParameter(): void
92 {
93 $this->actAsScopedUser(User::factory()->create(), ['*']);
94
95 $this
96 ->get(route('api.beatmaps.index'))
97 ->assertSuccessful();
98 }
99
100 public function testInvalidMode()
101 {
102 $this->json('GET', route('beatmaps.scores', $this->beatmap), [
103 'mode' => 'nope',
104 ])->assertStatus(422);
105 }
106
107 /**
108 * @dataProvider dataProviderForTestLookupForApi
109 */
110 public function testLookupForApi(string $key, callable $valueFn): void
111 {
112 $beatmap = Beatmap::factory()->create();
113
114 $this->actAsScopedUser(User::factory()->create(), ['*']);
115
116 $this
117 ->get(route('api.beatmaps.lookup', [$key => $valueFn($beatmap)]))
118 ->assertSuccessful()
119 ->assertJsonPath('id', $beatmap->getKey());
120 }
121
122 /**
123 * Make sure the lookup stops when finding beatmap from one of the parameters
124 */
125 public function testLookupMultipleParamsForApi(): void
126 {
127 $beatmap = Beatmap::factory()->create();
128
129 $this->actAsScopedUser(User::factory()->create(), ['*']);
130
131 $this
132 ->get(route('api.beatmaps.lookup', [
133 'checksum' => '',
134 'id' => (string) $beatmap->getKey(),
135 'filename' => '',
136 ]))
137 ->assertSuccessful()
138 ->assertJsonPath('id', $beatmap->getKey());
139 }
140
141 /**
142 * Checks whether HTTP 403 is thrown when a logged out
143 * user tries to access the non-general (country or friend ranking)
144 * scoreboards.
145 */
146 public function testScoresNonGeneralLoggedOut()
147 {
148 $this->json('GET', route('beatmaps.scores', $this->beatmap), [
149 'type' => 'country',
150 ])->assertStatus(422)
151 ->assertJson(['error' => osu_trans('errors.supporter_only')]);
152 }
153
154 /**
155 * Checks whether an error is thrown when an user without supporter
156 * tries to access supporter-only scoreboards.
157 */
158 public function testScoresNonGeneralSupporter()
159 {
160 $this->actingAs($this->user)
161 ->json('GET', route('beatmaps.scores', $this->beatmap), [
162 'type' => 'country',
163 ])->assertStatus(422)
164 ->assertJson(['error' => osu_trans('errors.supporter_only')]);
165
166 $this->user->osu_subscriber = true;
167 $this->user->save();
168
169 $this->actingAs($this->user)
170 ->json('GET', route('beatmaps.scores', $this->beatmap), [
171 'type' => 'country',
172 ])->assertStatus(200);
173 }
174
175 public function testShowForApi()
176 {
177 $beatmap = Beatmap::factory()->create();
178
179 $this->actAsScopedUser(User::factory()->create(), ['*']);
180
181 $this
182 ->get(route('api.beatmaps.show', ['beatmap' => $beatmap->getKey()]))
183 ->assertSuccessful()
184 ->assertJsonPath('id', $beatmap->getKey());
185 }
186
187 public static function dataProviderForTestLookupForApi(): array
188 {
189 return [
190 'checksum' => ['checksum', fn (Beatmap $b) => $b->checksum],
191 'filename' => ['filename', fn (Beatmap $b) => $b->filename],
192 'id' => ['id', fn (Beatmap $b) => $b->getKey()],
193 ];
194 }
195
196 public static function dataProviderForTestUpdateOwnerLoved(): array
197 {
198 return [
199 [Beatmapset::STATES['graveyard'], true],
200 [Beatmapset::STATES['loved'], true],
201 [Beatmapset::STATES['ranked'], false],
202 [Beatmapset::STATES['wip'], false],
203 ];
204 }
205
206 protected function setUp(): void
207 {
208 parent::setUp();
209
210 $this->user = User::factory()->create();
211 $this->beatmap = Beatmap::factory()->qualified()->create();
212 }
213
214 private function createExistingFruitsBeatmap()
215 {
216 return Beatmap::factory()->create([
217 'beatmap_id' => 2177697,
218 'beatmapset_id' => Beatmapset::factory(['beatmapset_id' => 918591]),
219 'playmode' => Beatmap::MODES['fruits'],
220 ]);
221 }
222
223 private function createExistingOsuBeatmap()
224 {
225 return Beatmap::factory()->create([
226 'beatmap_id' => 567606,
227 'beatmapset_id' => Beatmapset::factory(['beatmapset_id' => 246416]),
228 ]);
229 }
230}