Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IBX-8176: OpenAPI compatible location moving endpoint #95

Merged
merged 32 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e37a2e3
IBX-8176: Implemented routing language expression extension handling …
barw4 Jun 5, 2024
672d955
IBX-8176: CS
barw4 Jun 5, 2024
0d9bf93
IBX-8176: Added unit tests
barw4 Jun 5, 2024
44b355a
IBX-8176: Enable `strict_types`
barw4 Jun 5, 2024
960ab94
IBX-8176: Enable `strict_types`
barw4 Jun 5, 2024
f5b93b9
IBX-8176: Applied review remarks
barw4 Jun 5, 2024
0a1d1ff
IBX-8176: Fixed type
barw4 Jun 5, 2024
4bc4416
IBX-8176: Refactored solution
barw4 Jun 18, 2024
1d10088
IBX-8176: Removed 'future' implementation
barw4 Jun 18, 2024
ab90138
IBX-8176: Applied review remarks
barw4 Jun 19, 2024
e997219
IBX-8176: Fix route definition
barw4 Jun 19, 2024
395dd45
IBX-8176: Refactoring
barw4 Jun 19, 2024
b9dfeda
IBX-8176: Rename the expression function
barw4 Jun 19, 2024
d71e4b7
IBX-8176: Applied review remark
barw4 Jun 23, 2024
e8b74b9
IBX-8176: Added generation of `options` route for newly added `condit…
barw4 Jun 27, 2024
d426606
IBX-8176: Fixed tests
barw4 Jun 28, 2024
748cbd6
IBX-8176: Created new OpenAPI compatible location moving endpoint
barw4 May 20, 2024
593d73e
IBX-8176: Fixed functional tests
barw4 May 22, 2024
ad224fb
IBX-8176: Updated baseline for PHPStan
barw4 May 22, 2024
fa8ee0c
IBX-8176: Applied review remarks
barw4 May 23, 2024
91e6648
IBX-8176: Fixed parser logic
barw4 May 23, 2024
5017612
IBX-8176: Removed deprecation
barw4 May 27, 2024
12202f3
IBX-8176: Refactored routing to use the `is_content_type_compatible` …
barw4 Jun 6, 2024
b7bbf7b
IBX-8176: Used `Validator` to validate input data
barw4 Jun 6, 2024
9649869
IBX-8176: Applied review remarks
barw4 Jun 20, 2024
fd0e691
IBX-8176: Fix tests
barw4 Jun 20, 2024
585c297
IBX-8176: Fixed routing definition
barw4 Jun 24, 2024
5d0f1ee
IBX-8176: Added `options_route_suffix`
barw4 Jun 27, 2024
81fcc11
IBX-8176: Update `HttpOptionsTest`
barw4 Jun 27, 2024
2d38484
IBX-8176: Updated routing
barw4 Jun 28, 2024
8be4e88
IBX-8176: Fixed routing
barw4 Jul 2, 2024
6c6685c
IBX-8176: CS
barw4 Jul 2, 2024
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
5 changes: 0 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4035,11 +4035,6 @@ parameters:
count: 1
path: tests/bundle/Functional/LocationTest.php

-
message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Rest\\\\Functional\\\\LocationTest\\:\\:testMoveSubtree\\(\\) has no return type specified\\.$#"
count: 1
path: tests/bundle/Functional/LocationTest.php

-
message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Rest\\\\Functional\\\\LocationTest\\:\\:testMoveSubtree\\(\\) has parameter \\$locationHref with no type specified\\.$#"
count: 1
Expand Down
8 changes: 8 additions & 0 deletions src/bundle/Resources/config/input_parsers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -858,3 +858,11 @@ services:
$validator: '@validator'
tags:
- { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.internal.criterion.Image }

Ibexa\Rest\Server\Input\Parser\MoveLocation:
parent: Ibexa\Rest\Server\Common\Parser
arguments:
$locationService: '@ibexa.api.service.location'
$validator: '@validator'
tags:
- { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.MoveLocationInput }
10 changes: 10 additions & 0 deletions src/bundle/Resources/config/routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,16 @@ ibexa.rest.languages.view:
# Locations


ibexa.rest.move_location:
path: /content/locations/{locationPath}
controller: Ibexa\Rest\Server\Controller\Location::moveLocation
condition: 'ibexa_get_media_type(request) === "MoveLocationInput"'
methods: [POST]
options:
options_route_suffix: 'MoveLocationInput'
requirements:
locationPath: "[0-9/]+"

ibexa.rest.redirect_location:
path: /content/locations
defaults:
Expand Down
33 changes: 33 additions & 0 deletions src/lib/Server/Controller/Location.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,39 @@ public function moveSubtree($locationPath, Request $request)
}
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException
*/
public function moveLocation(Request $request, string $locationPath): Values\ResourceCreated
{
$destinationLocation = $this->inputDispatcher->parse(
new Message(
['Content-Type' => $request->headers->get('Content-Type')],
$request->getContent(),
),
);

$locationToMove = $this->locationService->loadLocation(
$this->extractLocationIdFromPath($locationPath),
);

$this->locationService->moveSubtree($locationToMove, $destinationLocation);

// Reload the location to get a new subtree position
$locationToMove = $this->locationService->loadLocation($locationToMove->id);

return new Values\ResourceCreated(
$this->router->generate(
'ibexa.rest.load_location',
[
'locationPath' => trim($locationToMove->getPathString(), '/'),
],
),
);
}

/**
* Swaps a location with another one.
*
Expand Down
84 changes: 84 additions & 0 deletions src/lib/Server/Input/Parser/MoveLocation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

barw4 marked this conversation as resolved.
Show resolved Hide resolved
namespace Ibexa\Rest\Server\Input\Parser;

use Ibexa\Contracts\Core\Repository\LocationService;
use Ibexa\Contracts\Core\Repository\Values\Content\Location;
use Ibexa\Contracts\Rest\Input\ParsingDispatcher;
use Ibexa\Rest\Input\BaseParser;
use Ibexa\Rest\Server\Exceptions\ValidationFailedException;
use Ibexa\Rest\Server\Validation\Builder\Input\Parser\MoveLocationInputValidatorBuilder;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class MoveLocation extends BaseParser
{
public const string DESTINATION_KEY = 'destination';

public function __construct(
private readonly LocationService $locationService,
private readonly ValidatorInterface $validator,
) {
}

/**
* @phpstan-param array{
* 'destination': string,
* } $data
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
* @throws \Ibexa\Contracts\Rest\Exceptions\Parser
*/
public function parse(array $data, ParsingDispatcher $parsingDispatcher): Location
{
$this->validateInputData($data);

return $this->getLocationByPath($data[self::DESTINATION_KEY]);
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
*/
private function getLocationByPath(string $path): Location
{
return $this->locationService->loadLocation(
$this->extractLocationIdFromPath($path)
);
}

private function extractLocationIdFromPath(string $path): int
{
$pathParts = explode('/', $path);

return (int)array_pop($pathParts);
}

/**
* @phpstan-assert array{
* 'destination': string,
* } $data
*
* @param array<mixed> $data
*
* @throws \Ibexa\Rest\Server\Exceptions\ValidationFailedException
*/
private function validateInputData(array $data): void
{
$builder = new MoveLocationInputValidatorBuilder($this->validator);
$builder->validateInputArray($data);
$violations = $builder->build()->getViolations();
if ($violations->count() > 0) {
throw new ValidationFailedException(
'MoveLocation',
$violations,
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Rest\Server\Validation\Builder\Input\Parser;

use Ibexa\Rest\Server\Input\Parser\MoveLocation;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints as Assert;

final class MoveLocationInputValidatorBuilder extends BaseInputParserValidatorBuilder
{
protected function buildConstraint(): Constraint
{
return new Assert\Collection(
[
MoveLocation::DESTINATION_KEY => [
new Assert\NotBlank(),
new Assert\Type('string'),
new Assert\Regex('/^(\/\d+)+$/'),
],
],
);
}
}
1 change: 1 addition & 0 deletions tests/bundle/Functional/HttpOptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public function providerForTestHttpOptions(): array
['/content/objectstategroups/1/objectstates/1', ['GET', 'PATCH', 'DELETE']],
['/content/objects/1/objectstates', ['GET', 'PATCH']],
['/content/locations', ['GET']],
['/content/locations/1/2', ['POST'], 'MoveLocationInput+json'],
['/content/locations/1/2', ['GET', 'PATCH', 'DELETE', 'COPY', 'MOVE', 'SWAP']],
['/content/locations/1/2/children', ['GET']],
['/content/objects/1/locations', ['GET', 'POST']],
Expand Down
23 changes: 22 additions & 1 deletion tests/bundle/Functional/LocationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public function testCopySubtree($locationHref)
*
* @depends testCopySubtree
*/
public function testMoveSubtree($locationHref)
public function testMoveSubtree($locationHref): string
{
$request = $this->createHttpRequest(
'MOVE',
Expand All @@ -153,6 +153,8 @@ public function testMoveSubtree($locationHref)

self::assertHttpResponseCodeEquals($response, 201);
self::assertHttpResponseHasHeader($response, 'Location');

return $locationHref;
}

/**
Expand Down Expand Up @@ -262,4 +264,23 @@ private function createUrlAlias(string $locationHref, string $urlAlias): string

return $href;
}

/**
* @depends testMoveSubtree
*/
public function testMoveLocation(string $locationHref): void
{
$request = $this->createHttpRequest(
'POST',
$locationHref,
'MoveLocationInput+json',
'',
json_encode(['MoveLocationInput' => ['destination' => '/1/2']], JSON_THROW_ON_ERROR),
);

$response = $this->sendHttpRequest($request);

self::assertHttpResponseCodeEquals($response, 201);
self::assertHttpResponseHasHeader($response, 'Location');
}
}
105 changes: 105 additions & 0 deletions tests/lib/Server/Input/Parser/MoveLocationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

barw4 marked this conversation as resolved.
Show resolved Hide resolved
namespace Ibexa\Tests\Rest\Server\Input\Parser;

use Ibexa\Contracts\Core\Repository\LocationService;
use Ibexa\Core\Repository\Values\Content\Location;
use Ibexa\Rest\Server\Exceptions\ValidationFailedException;
use Ibexa\Rest\Server\Input\Parser\MoveLocation;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class MoveLocationTest extends BaseTest
{
private const int TESTED_LOCATION_ID = 22;

private MockObject&LocationService $locationService;

private ValidatorInterface $validator;

public function testParse(): void
{
$destinationPath = sprintf('/1/2/%d', self::TESTED_LOCATION_ID);

$inputArray = [
'destination' => $destinationPath,
];

$moveLocationParser = $this->getParser();

$this->locationService
->expects(self::once())
->method('loadLocation')
->with(self::TESTED_LOCATION_ID)
->willReturn($this->getMockedLocation());

$result = $moveLocationParser->parse($inputArray, $this->getParsingDispatcherMock());

self::assertEquals(
$this->getMockedLocation()->id,
$result->id,
);

self::assertEquals(
$this->getMockedLocation()->getPathString(),
$result->getPathString(),
);
}

public function testParseExceptionOnMissingDestinationElement(): void
{
$this->expectException(ValidationFailedException::class);
$this->expectExceptionMessage('Input data validation failed for MoveLocation');

$inputArray = [
'new_destination' => '/1/2/3',
];

$sessionInput = $this->getParser();

$sessionInput->parse($inputArray, $this->getParsingDispatcherMock());
}

public function testParseExceptionOnInvalidDestinationElement(): void
{
$inputArray = [
'destination' => 'test_destination',
];

$sessionInput = $this->getParser();

$this->expectException(ValidationFailedException::class);
$this->expectExceptionMessage('Input data validation failed for MoveLocation');

$sessionInput->parse($inputArray, $this->getParsingDispatcherMock());
}

protected function internalGetParser(): MoveLocation
{
$locationService = $this->createMock(LocationService::class);
$this->locationService = $locationService;
$this->validator = Validation::createValidator();

return new MoveLocation(
$this->locationService,
$this->validator,
);
}

private function getMockedLocation(): Location
{
return new Location(
[
'id' => self::TESTED_LOCATION_ID,
'pathString' => sprintf('/1/2/%d', self::TESTED_LOCATION_ID),
],
);
}
}
Loading