Skip to content

Commit 0c7db21

Browse files
Do expose contests before start in the API
Fixes #3166. Also return 404 for problemset before contest start instead of 403.
1 parent 94e92c5 commit 0c7db21

File tree

3 files changed

+71
-8
lines changed

3 files changed

+71
-8
lines changed

webapp/src/Controller/API/AbstractApiController.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ public function __construct(
2929
protected readonly EventLogService $eventLogService
3030
) {}
3131

32+
/**
33+
* Whether to filter out contests that haven't started yet in calls to getQueryBuilder
34+
* without an explicit value set for $filterBeforeContest.
35+
* Override in subclasses to change this behavior.
36+
*/
37+
protected function shouldFilterBeforeContest(): bool
38+
{
39+
return true;
40+
}
41+
3242
/**
3343
* Get the query builder used for getting contests.
3444
*

webapp/src/Controller/API/ContestController.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -356,24 +356,24 @@ public function setProblemsetAction(Request $request, string $cid, ValidatorInte
356356
public function problemsetAction(Request $request, string $cid): Response
357357
{
358358
/** @var Contest|null $contest */
359-
$contest = $this->getQueryBuilder($request)
359+
$contest = $this->getQueryBuilder($request, filterBeforeContest: true)
360360
->andWhere(sprintf('%s = :id', $this->getIdField()))
361361
->setParameter('id', $cid)
362362
->getQuery()
363363
->getOneOrNullResult();
364364

365+
if ($contest === null) {
366+
throw new NotFoundHttpException(sprintf('Object with ID \'%s\' not found', $cid));
367+
}
368+
365369
$hasAccess = $this->dj->checkrole('jury') ||
366370
$this->dj->checkrole('api_reader') ||
367-
$contest?->getFreezeData()->started();
371+
$contest->getFreezeData()->started();
368372

369373
if (!$hasAccess) {
370374
throw new AccessDeniedHttpException();
371375
}
372376

373-
if ($contest === null) {
374-
throw new NotFoundHttpException(sprintf('Object with ID \'%s\' not found', $cid));
375-
}
376-
377377
if (!$contest->getContestProblemsetType()) {
378378
throw new NotFoundHttpException(sprintf('Contest with ID \'%s\' has no problemset text', $cid));
379379
}
@@ -950,10 +950,18 @@ public function samplesDataZipAction(Request $request): Response
950950
return $this->dj->getSamplesZipForContest($contest);
951951
}
952952

953-
protected function getQueryBuilder(Request $request, bool $filterBeforeContest = true): QueryBuilder
953+
protected function shouldFilterBeforeContest(): bool
954+
{
955+
return false;
956+
}
957+
958+
protected function getQueryBuilder(Request $request, ?bool $filterBeforeContest = null): QueryBuilder
954959
{
955960
try {
956-
return $this->getContestQueryBuilder($request->query->getBoolean('onlyActive', true), $filterBeforeContest);
961+
return $this->getContestQueryBuilder(
962+
$request->query->getBoolean('onlyActive', true),
963+
$filterBeforeContest ?? $this->shouldFilterBeforeContest()
964+
);
957965
} catch (TypeError) {
958966
throw new BadRequestHttpException('\'onlyActive\' must be a boolean.');
959967
}

webapp/tests/Unit/Controller/API/ContestControllerTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Tests\Unit\Controller\API;
44

5+
use App\DataFixtures\Test\DemoPreStartContestFixture;
56
use App\Entity\Contest;
67

78
class ContestControllerTest extends BaseTestCase
@@ -26,4 +27,48 @@ class ContestControllerTest extends BaseTestCase
2627
protected array $expectedAbsent = ['4242', 'nonexistent'];
2728

2829
protected ?string $objectClassForExternalId = Contest::class;
30+
31+
/**
32+
* Test that a contest that is activated but not yet started is visible in the list action for a team user.
33+
*/
34+
public function testListShowsActivatedButNotStartedContest(): void
35+
{
36+
$this->loadFixture(DemoPreStartContestFixture::class);
37+
38+
$url = $this->helperGetEndpointURL($this->apiEndpoint);
39+
// Use 'demo' user which has team role
40+
$objects = $this->verifyApiJsonResponse('GET', $url, 200, 'demo');
41+
42+
self::assertIsArray($objects);
43+
self::assertNotEmpty($objects, 'Contest list should not be empty');
44+
45+
// Find the demo contest in the response
46+
$foundContest = null;
47+
foreach ($objects as $contest) {
48+
if ($contest['shortname'] === 'demo') {
49+
$foundContest = $contest;
50+
break;
51+
}
52+
}
53+
54+
self::assertNotNull($foundContest, 'Demo contest should be visible after activation even before start');
55+
self::assertSame('Demo contest', $foundContest['formal_name']);
56+
}
57+
58+
/**
59+
* Test that a contest that is activated but not yet started is visible in the single action for a team user.
60+
*/
61+
public function testSingleShowsActivatedButNotStartedContest(): void
62+
{
63+
$this->loadFixture(DemoPreStartContestFixture::class);
64+
65+
$contestId = $this->resolveEntityId(Contest::class, '1');
66+
$url = $this->helperGetEndpointURL($this->apiEndpoint, $contestId);
67+
// Use 'demo' user which has team role
68+
$contest = $this->verifyApiJsonResponse('GET', $url, 200, 'demo');
69+
70+
self::assertIsArray($contest);
71+
self::assertSame('Demo contest', $contest['formal_name']);
72+
self::assertSame('demo', $contest['shortname']);
73+
}
2974
}

0 commit comments

Comments
 (0)