Skip to content

Commit

Permalink
IBX-8176: OpenAPI compatible location moving endpoint (#95)
Browse files Browse the repository at this point in the history
* IBX-8176: Implemented routing language expression extension handling `Content-Type` header

* IBX-8176: CS

* IBX-8176: Added unit tests

* IBX-8176: Enable `strict_types`

* IBX-8176: Enable `strict_types`

* IBX-8176: Applied review remarks

* IBX-8176: Fixed type

* IBX-8176: Refactored solution

* IBX-8176: Removed 'future' implementation

* IBX-8176: Applied review remarks

* IBX-8176: Fix route definition

* IBX-8176: Refactoring

* IBX-8176: Rename the expression function

* IBX-8176: Applied review remark

* IBX-8176: Added generation of `options` route for newly added `condition`-dependable routes

* IBX-8176: Fixed tests

* IBX-8176: Created new OpenAPI compatible location moving endpoint

* IBX-8176: Fixed functional tests

* IBX-8176: Updated baseline for PHPStan

* IBX-8176: Applied review remarks

* IBX-8176: Fixed parser logic

* IBX-8176: Removed deprecation

* IBX-8176: Refactored routing to use the `is_content_type_compatible` function

* IBX-8176: Used `Validator` to validate input data

* IBX-8176: Applied review remarks

* IBX-8176: Fix tests

* IBX-8176: Fixed routing definition

* IBX-8176: Added `options_route_suffix`

* IBX-8176: Update `HttpOptionsTest`

* IBX-8176: Updated routing

* IBX-8176: Fixed routing

* IBX-8176: CS
  • Loading branch information
barw4 authored Jul 12, 2024
1 parent 47ae01a commit 3d53e1f
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 6 deletions.
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);

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);

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),
],
);
}
}

0 comments on commit 3d53e1f

Please sign in to comment.