Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 63 additions & 11 deletions webapp/src/Controller/API/GroupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Controller\API;

use App\DataTransferObject\TeamCategoryPost;
use App\DataTransferObject\TeamCategoryPut;
use App\Entity\TeamCategory;
use App\Service\ImportExportService;
use Doctrine\ORM\NonUniqueResultException;
Expand Down Expand Up @@ -91,19 +92,70 @@ public function addAction(
#[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]
TeamCategoryPost $teamCategoryPost,
Request $request,
ImportExportService $importExport
ImportExportService $importExport,
): Response {
$saved = [];
$importExport->importGroupsJson([
[
'name' => $teamCategoryPost->name,
'hidden' => $teamCategoryPost->hidden,
'icpc_id' => $teamCategoryPost->icpcId,
'sortorder' => $teamCategoryPost->sortorder,
'color' => $teamCategoryPost->color,
'allow_self_registration' => $teamCategoryPost->allowSelfRegistration,
],
], $message, $saved);
$groupData = [
'name' => $teamCategoryPost->name,
'hidden' => $teamCategoryPost->hidden,
'icpc_id' => $teamCategoryPost->icpcId,
'sortorder' => $teamCategoryPost->sortorder,
'color' => $teamCategoryPost->color,
'allow_self_registration' => $teamCategoryPost->allowSelfRegistration,
];
$importExport->importGroupsJson([$groupData], $message, $saved);
if (!empty($message)) {
throw new BadRequestHttpException("Error while adding group: $message");
}

$group = $saved[0];
$idField = $this->eventLogService->externalIdFieldForEntity(TeamCategory::class) ?? 'categoryid';
$method = sprintf('get%s', ucfirst($idField));
$id = call_user_func([$group, $method]);

return $this->renderCreateData($request, $saved[0], 'group', $id);
}

/**
* Update an existing group or create one with the given ID
*/
#[IsGranted('ROLE_API_WRITER')]
#[Rest\Put('/{id}')]
#[OA\RequestBody(
required: true,
content: [
new OA\MediaType(
mediaType: 'multipart/form-data',
schema: new OA\Schema(ref: new Model(type: TeamCategoryPut::class))
),
]
)]
#[OA\Response(
response: 201,
description: 'Returns the updated / added group',
content: new Model(type: TeamCategory::class)
)]
public function updateAction(
#[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]
TeamCategoryPut $teamCategoryPut,
Request $request,
ImportExportService $importExport,
string $id,
): Response {
$saved = [];
$groupData = [
'id' => $teamCategoryPut->id,
'name' => $teamCategoryPut->name,
'hidden' => $teamCategoryPut->hidden,
'icpc_id' => $teamCategoryPut->icpcId,
'sortorder' => $teamCategoryPut->sortorder,
'color' => $teamCategoryPut->color,
'allow_self_registration' => $teamCategoryPut->allowSelfRegistration,
];
if ($id !== $teamCategoryPut->id) {
throw new BadRequestHttpException('ID in URL does not match ID in payload');
}
$importExport->importGroupsJson([$groupData], $message, $saved);
if (!empty($message)) {
throw new BadRequestHttpException("Error while adding group: $message");
}
Expand Down
57 changes: 52 additions & 5 deletions webapp/src/Controller/API/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Controller\API;

use App\DataTransferObject\AddUser;
use App\DataTransferObject\UpdateUser;
use App\Entity\Role;
use App\Entity\Team;
use App\Entity\User;
Expand Down Expand Up @@ -64,7 +65,7 @@ public function __construct(
description: 'The groups.json files to import.',
type: 'string',
format: 'binary'
)
),
]
)
)
Expand Down Expand Up @@ -106,7 +107,7 @@ public function addGroupsAction(Request $request): string
property: 'json',
description: 'The organizations.json files to import.',
type: 'string',
format: 'binary')
format: 'binary'),
]
)
)
Expand Down Expand Up @@ -150,7 +151,7 @@ public function addOrganizationsAction(Request $request): string
description: 'The teams.json files to import.',
type: 'string',
format: 'binary'
)
),
]
)
)
Expand Down Expand Up @@ -205,7 +206,7 @@ public function addTeamsAction(Request $request): string
description: 'The accounts.yaml files to import.',
type: 'string',
format: 'binary'
)
),
]
)
)
Expand Down Expand Up @@ -294,7 +295,7 @@ public function singleAction(Request $request, string $id): Response
new OA\MediaType(
mediaType: 'multipart/form-data',
schema: new OA\Schema(ref: new Model(type: AddUser::class))
)
),
]
)]
#[OA\Response(
Expand All @@ -307,11 +308,53 @@ public function addAction(
AddUser $addUser,
Request $request
): Response {
return $this->addOrUpdateUser($addUser, $request);
}

/**
* Update an existing User or create one with the given ID
*/
#[IsGranted('ROLE_API_WRITER')]
#[Rest\Put('/{id}')]
#[OA\RequestBody(
required: true,
content: [
new OA\MediaType(
mediaType: 'multipart/form-data',
schema: new OA\Schema(ref: new Model(type: UpdateUser::class))
),
]
)]
#[OA\Response(
response: 201,
description: 'Returns the added user',
content: new Model(type: User::class)
)]
public function updateAction(
#[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]
UpdateUser $updateUser,
Request $request
): Response {
return $this->addOrUpdateUser($updateUser, $request);
}

protected function addOrUpdateUser(AddUser $addUser, Request $request): Response
{
if ($addUser instanceof UpdateUser && $this->eventLogService->externalIdFieldForEntity(User::class) && !$addUser->id) {
throw new BadRequestHttpException('`id` field is required');
}

if ($this->em->getRepository(User::class)->findOneBy(['username' => $addUser->username])) {
throw new BadRequestHttpException(sprintf("User %s already exists", $addUser->username));
}

$user = new User();
if ($addUser instanceof UpdateUser) {
$existingUser = $this->em->getRepository(User::class)->findOneBy([$this->eventLogService->externalIdFieldForEntity(User::class) => $addUser->id]);
if ($existingUser) {
$user = $existingUser;
}
}
$user
->setUsername($addUser->username)
->setName($addUser->name)
Expand All @@ -320,6 +363,10 @@ public function addAction(
->setPlainPassword($addUser->password)
->setEnabled($addUser->enabled ?? true);

if ($addUser instanceof UpdateUser) {
$user->setExternalid($addUser->id);
}

if ($addUser->teamId) {
/** @var Team|null $team */
$team = $this->em->createQueryBuilder()
Expand Down
22 changes: 22 additions & 0 deletions webapp/src/DataTransferObject/TeamCategoryPut.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types=1);

namespace App\DataTransferObject;

use OpenApi\Attributes as OA;

#[OA\Schema(required: ['id', 'name'])]
class TeamCategoryPut extends TeamCategoryPost
{
public function __construct(
#[OA\Property(description: 'The ID of the group. Only allowed with PUT requests', nullable: true)]
public readonly ?string $id,
string $name,
bool $hidden = false,
?string $icpcId = null,
int $sortorder = 0,
?string $color = null,
bool $allowSelfRegistration = false
) {
parent::__construct($name, $hidden, $icpcId, $sortorder, $color, $allowSelfRegistration);
}
}
23 changes: 23 additions & 0 deletions webapp/src/DataTransferObject/UpdateUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php declare(strict_types=1);

namespace App\DataTransferObject;

use OpenApi\Attributes as OA;

#[OA\Schema(required: ['id', 'username', 'name', 'roles'])]
class UpdateUser extends AddUser
{
public function __construct(
public readonly string $id,
string $username,
string $name,
?string $email,
?string $ip,
?string $password,
?bool $enabled,
?string $teamId,
array $roles
) {
parent::__construct($username, $name, $email, $ip, $password, $enabled, $teamId, $roles);
}
}
55 changes: 55 additions & 0 deletions webapp/tests/Unit/Controller/API/GroupControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Tests\Unit\Controller\API;

use App\Service\DOMJudgeService;
use Generator;

class GroupControllerTest extends BaseTestCase
Expand Down Expand Up @@ -85,6 +86,60 @@ public function testNewAddedGroupPostWithId(): void
self::assertNotEquals($returnedObject['id'], $postWithId['id']);
}

/**
* @dataProvider provideNewAddedGroup
*/
public function testNewAddedGroupPut(array $newGroupPostData): void
{
// This only works for non-local data sources
$this->setupDataSource(DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL);

$url = $this->helperGetEndpointURL($this->apiEndpoint);
$objectsBeforeTest = $this->verifyApiJsonResponse('GET', $url, 200, $this->apiUser);

$newGroupPostData['id'] = 'someid';

$returnedObject = $this->verifyApiJsonResponse('PUT', $url . '/someid', 201, 'admin', $newGroupPostData);
foreach ($newGroupPostData as $key => $value) {
self::assertEquals($value, $returnedObject[$key]);
}

$objectsAfterTest = $this->verifyApiJsonResponse('GET', $url, 200, $this->apiUser);
$newItems = array_map('unserialize', array_diff(array_map('serialize', $objectsAfterTest), array_map('serialize', $objectsBeforeTest)));
self::assertEquals(1, count($newItems));
$listKey = array_keys($newItems)[0];
foreach ($newGroupPostData as $key => $value) {
self::assertEquals($value, $newItems[$listKey][$key]);
}
}

/**
* @dataProvider provideNewAddedGroup
*/
public function testNewAddedGroupPutWithoutId(array $newGroupPostData): void
{
// This only works for non-local data sources
$this->setupDataSource(DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL);

$url = $this->helperGetEndpointURL($this->apiEndpoint);
$returnedObject = $this->verifyApiJsonResponse('PUT', $url . '/someid', 400, 'admin', $newGroupPostData);
self::assertStringContainsString('ID in URL does not match ID in payload', $returnedObject['message']);
}

/**
* @dataProvider provideNewAddedGroup
*/
public function testNewAddedGroupPutWithDifferentId(array $newGroupPostData): void
{
// This only works for non-local data sources
$this->setupDataSource(DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL);

$newGroupPostData['id'] = 'someotherid';
$url = $this->helperGetEndpointURL($this->apiEndpoint);
$returnedObject = $this->verifyApiJsonResponse('PUT', $url . '/someid', 400, 'admin', $newGroupPostData);
self::assertStringContainsString('ID in URL does not match ID in payload', $returnedObject['message']);
}

public function provideNewAddedGroup(): Generator
{
foreach ($this->newGroupsPostData as $group) {
Expand Down
50 changes: 50 additions & 0 deletions webapp/tests/Unit/Controller/API/UserControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace App\Tests\Unit\Controller\API;

use App\Service\DOMJudgeService;

class UserControllerTest extends AccountBaseTestCase
{
protected ?string $apiEndpoint = 'users';
Expand Down Expand Up @@ -45,4 +47,52 @@ class UserControllerTest extends AccountBaseTestCase
"enabled" => true
],
];

public function testAddLocal(): void
{
$data = [
'username' => 'testuser',
'name' => 'Test User',
'roles' => ['team'],
'password' => 'testpassword',
];

$response = $this->verifyApiJsonResponse('POST', $this->helperGetEndpointURL($this->apiEndpoint), 201, 'admin', $data);
static::assertArrayHasKey('id', $response);
static::assertEquals('testuser', $response['username']);
static::assertEquals('Test User', $response['name']);
static::assertEquals(['team'], $response['roles']);
}

public function testUpdateNonLocal(): void
{
$this->setupDataSource(DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL);
$data = [
'id' => 'someid',
'username' => 'testuser',
'name' => 'Test User',
'roles' => ['team'],
'password' => 'testpassword',
];

$response = $this->verifyApiJsonResponse('PUT', $this->helperGetEndpointURL($this->apiEndpoint) . '/someid', 201, 'admin', $data);
static::assertEquals('someid', $response['id']);
static::assertEquals('testuser', $response['username']);
static::assertEquals('Test User', $response['name']);
static::assertEquals(['team'], $response['roles']);
}

public function testUpdateNonLocalNoId(): void
{
$this->setupDataSource(DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL);
$data = [
'username' => 'testuser',
'name' => 'Test User',
'roles' => ['team'],
'password' => 'testpassword',
];

$response = $this->verifyApiJsonResponse('PUT', $this->helperGetEndpointURL($this->apiEndpoint) . '/someid', 400, 'admin', $data);
static::assertMatchesRegularExpression('/id:\n.*This value should be of type unknown./', $response['message']);
}
}