Skip to content

Commit f616fd2

Browse files
committed
API for private players protection + tests
1 parent dd087ed commit f616fd2

8 files changed

Lines changed: 464 additions & 251 deletions

src/Api/V1/PlayerResultsResponseProvider.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use ApiPlatform\Metadata\Operation;
88
use ApiPlatform\State\ProviderInterface;
9+
use SpeedPuzzling\Web\Query\GetPlayerProfile;
910
use SpeedPuzzling\Web\Query\GetPlayerSolvedPuzzles;
1011
use SpeedPuzzling\Web\Results\SolvedPuzzle;
1112
use Symfony\Component\HttpFoundation\RequestStack;
@@ -18,6 +19,7 @@
1819
public function __construct(
1920
private GetPlayerSolvedPuzzles $getPlayerSolvedPuzzles,
2021
private RequestStack $requestStack,
22+
private GetPlayerProfile $getPlayerProfile,
2123
) {
2224
}
2325

@@ -26,9 +28,20 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
2628
/** @var string $playerId */
2729
$playerId = $uriVariables['playerId'];
2830

31+
$profile = $this->getPlayerProfile->byId($playerId);
32+
2933
$request = $this->requestStack->getCurrentRequest();
3034
$type = $request?->query->getString('type', 'solo') ?? 'solo';
3135

36+
if ($profile->isPrivate) {
37+
return new PlayerResultsResponse(
38+
player_id: $playerId,
39+
type: $type,
40+
count: 0,
41+
results: [],
42+
);
43+
}
44+
3245
$results = match ($type) {
3346
'duo' => $this->getPlayerSolvedPuzzles->duoByPlayerId($playerId),
3447
'team' => $this->getPlayerSolvedPuzzles->teamByPlayerId($playerId),

src/Api/V1/PlayerStatisticsResponseProvider.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use ApiPlatform\Metadata\Operation;
88
use ApiPlatform\State\ProviderInterface;
9+
use SpeedPuzzling\Web\Query\GetPlayerProfile;
910
use SpeedPuzzling\Web\Query\GetPlayerStatistics;
1011
use SpeedPuzzling\Web\Results\PlayerStatistics;
1112

@@ -16,6 +17,7 @@
1617
{
1718
public function __construct(
1819
private GetPlayerStatistics $getPlayerStatistics,
20+
private GetPlayerProfile $getPlayerProfile,
1921
) {
2022
}
2123

@@ -24,6 +26,23 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
2426
/** @var string $playerId */
2527
$playerId = $uriVariables['playerId'];
2628

29+
$profile = $this->getPlayerProfile->byId($playerId);
30+
31+
$emptyStats = new StatisticsGroupResponse(
32+
total_seconds: 0,
33+
total_pieces: 0,
34+
solved_puzzles_count: 0,
35+
);
36+
37+
if ($profile->isPrivate) {
38+
return new PlayerStatisticsResponse(
39+
player_id: $playerId,
40+
solo: $emptyStats,
41+
duo: $emptyStats,
42+
team: $emptyStats,
43+
);
44+
}
45+
2746
$solo = $this->getPlayerStatistics->solo($playerId);
2847
$duo = $this->getPlayerStatistics->duo($playerId);
2948
$team = $this->getPlayerStatistics->team($playerId);

src/Controller/Api/V0/GetPlayerResultsController.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace SpeedPuzzling\Web\Controller\Api\V0;
66

77
use SpeedPuzzling\Web\Exceptions\PlayerNotFound;
8+
use SpeedPuzzling\Web\Query\GetPlayerProfile;
89
use SpeedPuzzling\Web\Query\GetPlayerSolvedPuzzles;
910
use SpeedPuzzling\Web\Results\SolvedPuzzle;
1011
use SpeedPuzzling\Web\Services\PuzzlingTimeFormatter;
@@ -22,6 +23,7 @@ public function __construct(
2223
readonly private GetPlayerSolvedPuzzles $getPlayerSolvedPuzzles,
2324
readonly private PuzzlingTimeFormatter $puzzlingTimeFormatter,
2425
readonly private RelativeTimeFormatter $relativeTimeFormatter,
26+
readonly private GetPlayerProfile $getPlayerProfile,
2527
) {
2628
}
2729

@@ -35,6 +37,16 @@ public function __invoke(
3537
}
3638

3739
try {
40+
$profile = $this->getPlayerProfile->byId($playerId);
41+
42+
if ($profile->isPrivate) {
43+
return $this->json([
44+
'solo' => [],
45+
'duo' => [],
46+
'team' => [],
47+
]);
48+
}
49+
3850
$soloResults = array_map(
3951
fn (SolvedPuzzle $result): array => $this->resultToApiShape($result),
4052
$this->getPlayerSolvedPuzzles->soloByPlayerId($playerId),
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SpeedPuzzling\Web\Tests\Controller\Api\V0;
6+
7+
use SpeedPuzzling\Web\Tests\DataFixtures\PlayerFixture;
8+
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
9+
10+
final class GetPlayerResultsControllerTest extends WebTestCase
11+
{
12+
public function testPrivatePlayerReturnsEmptyResults(): void
13+
{
14+
$browser = self::createClient();
15+
16+
$browser->request('GET', '/api/v0/players/' . PlayerFixture::PLAYER_PRIVATE . '/results?token=any');
17+
18+
$this->assertResponseIsSuccessful();
19+
20+
$responseContent = $browser->getResponse()->getContent();
21+
$this->assertIsString($responseContent);
22+
23+
/** @var array{solo: array<mixed>, duo: array<mixed>, team: array<mixed>} $response */
24+
$response = json_decode($responseContent, true, 512, JSON_THROW_ON_ERROR);
25+
26+
$this->assertSame([], $response['solo']);
27+
$this->assertSame([], $response['duo']);
28+
$this->assertSame([], $response['team']);
29+
}
30+
31+
public function testPublicPlayerReturnsResults(): void
32+
{
33+
$browser = self::createClient();
34+
35+
$browser->request('GET', '/api/v0/players/' . PlayerFixture::PLAYER_REGULAR . '/results?token=any');
36+
37+
$this->assertResponseIsSuccessful();
38+
39+
$responseContent = $browser->getResponse()->getContent();
40+
$this->assertIsString($responseContent);
41+
42+
/** @var array<string, mixed> $response */
43+
$response = json_decode($responseContent, true, 512, JSON_THROW_ON_ERROR);
44+
45+
$this->assertArrayHasKey('solo', $response);
46+
$this->assertArrayHasKey('duo', $response);
47+
$this->assertArrayHasKey('team', $response);
48+
}
49+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SpeedPuzzling\Web\Tests\Controller\Api\V1;
6+
7+
use SpeedPuzzling\Web\Tests\DataFixtures\OAuth2ClientFixture;
8+
use SpeedPuzzling\Web\Tests\DataFixtures\PlayerFixture;
9+
use SpeedPuzzling\Web\Tests\OAuth2TestHelper;
10+
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
11+
use Symfony\Component\HttpFoundation\Response;
12+
13+
final class CurrentUserEndpointTest extends WebTestCase
14+
{
15+
public function testWithoutTokenReturnsUnauthorized(): void
16+
{
17+
$browser = self::createClient();
18+
19+
$browser->request('GET', '/api/v1/me');
20+
21+
$this->assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED);
22+
}
23+
24+
public function testWithInvalidTokenReturnsUnauthorized(): void
25+
{
26+
$browser = self::createClient();
27+
28+
OAuth2TestHelper::addBearerToken($browser, 'invalid-token');
29+
$browser->request('GET', '/api/v1/me');
30+
31+
$this->assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED);
32+
}
33+
34+
public function testWithValidTokenReturnsUserProfile(): void
35+
{
36+
$browser = self::createClient();
37+
38+
$token = OAuth2TestHelper::createAccessToken(
39+
$browser,
40+
OAuth2ClientFixture::CONFIDENTIAL_CLIENT_ID,
41+
PlayerFixture::PLAYER_REGULAR,
42+
['profile:read'],
43+
);
44+
45+
OAuth2TestHelper::addBearerToken($browser, $token);
46+
$browser->request('GET', '/api/v1/me');
47+
48+
$this->assertResponseIsSuccessful();
49+
50+
$responseContent = $browser->getResponse()->getContent();
51+
$this->assertIsString($responseContent);
52+
53+
/** @var array{id: string, name: string, code?: string, avatar?: string, country?: string} $response */
54+
$response = json_decode($responseContent, true, 512, JSON_THROW_ON_ERROR);
55+
56+
$this->assertSame(PlayerFixture::PLAYER_REGULAR, $response['id']);
57+
$this->assertSame(PlayerFixture::PLAYER_REGULAR_NAME, $response['name']);
58+
$this->assertArrayHasKey('code', $response);
59+
$this->assertArrayHasKey('avatar', $response);
60+
$this->assertArrayHasKey('country', $response);
61+
}
62+
63+
public function testWithoutRequiredScopeReturnsForbidden(): void
64+
{
65+
$browser = self::createClient();
66+
67+
$token = OAuth2TestHelper::createAccessToken(
68+
$browser,
69+
OAuth2ClientFixture::CONFIDENTIAL_CLIENT_ID,
70+
PlayerFixture::PLAYER_REGULAR,
71+
['results:read'], // Wrong scope
72+
);
73+
74+
OAuth2TestHelper::addBearerToken($browser, $token);
75+
$browser->request('GET', '/api/v1/me');
76+
77+
$this->assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN);
78+
}
79+
80+
public function testWorksForPrivatePlayer(): void
81+
{
82+
$browser = self::createClient();
83+
84+
$token = OAuth2TestHelper::createAccessToken(
85+
$browser,
86+
OAuth2ClientFixture::CONFIDENTIAL_CLIENT_ID,
87+
PlayerFixture::PLAYER_PRIVATE,
88+
['profile:read'],
89+
);
90+
91+
OAuth2TestHelper::addBearerToken($browser, $token);
92+
$browser->request('GET', '/api/v1/me');
93+
94+
$this->assertResponseIsSuccessful();
95+
96+
$responseContent = $browser->getResponse()->getContent();
97+
$this->assertIsString($responseContent);
98+
99+
/** @var array{id: string, name: string, is_private: bool} $response */
100+
$response = json_decode($responseContent, true, 512, JSON_THROW_ON_ERROR);
101+
102+
$this->assertSame(PlayerFixture::PLAYER_PRIVATE, $response['id']);
103+
$this->assertTrue($response['is_private']);
104+
}
105+
}

0 commit comments

Comments
 (0)