Commit e646a166 authored by Oliver Bartsch's avatar Oliver Bartsch

[TASK] Kickstart tests for ter_rest extension

parent a56839bf
Pipeline #10047 failed with stages
in 3 minutes and 11 seconds
......@@ -17,10 +17,14 @@
<testsuite name="ter_fe2 tests">
<directory>../../extensions/ter_fe2/Tests/Unit</directory>
</testsuite>
<testsuite name="ter_rest tests">
<directory>../../extensions/ter_rest/Tests/Unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">../../extensions/ter_fe2/Classes/</directory>
<directory suffix=".php">../../extensions/ter_rest/Classes/</directory>
</whitelist>
</filter>
<logging>
......
......@@ -44,7 +44,7 @@ class Scope extends BitSet
public static function convert(array $scopes): int
{
$scope = static::EXTENSION_NONE;
foreach ($scopes as $name) {
foreach (array_unique($scopes) as $name) {
$scope += static::getScope($name);
}
return $scope;
......
......@@ -33,7 +33,7 @@ class ArrayArgument extends AbstractRouteArgument implements DataStructureRouteA
if (!is_array($value)) {
throw new RouteArgumentValueException(
sprintf('Value \'%s\' for argument \'%s\' can\'t be interpredet as array value.', $value, $name),
1601313518
1606151911
);
}
......
......@@ -26,9 +26,9 @@ class ObjectArgument extends AbstractRouteArgument implements DataStructureRoute
public function __construct(string $name, array $configuration, $value)
{
if (!is_array($value)) {
if (!is_array($value) || $value === []) {
throw new RouteArgumentValueException(
sprintf('Value \'%s\' for argument \'%s\' must be of type array.', $value, $name),
sprintf('Value \'%s\' for argument \'%s\' must be of type array and must not be empty.', $value, $name),
1601313412
);
}
......
<?php
declare(strict_types = 1);
namespace T3o\TerRest\Tests\Unit\Authentication;
/*
* This file is part of TYPO3 CMS-extension "ter_rest", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
use Nimut\TestingFramework\TestCase\AbstractTestCase;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Container;
use T3o\TerRest\Authentication\AuthenticationDispatcher;
use T3o\TerRest\Authentication\AuthenticationHandlerInterface;
use T3o\TerRest\Authentication\User\ApiUserInterface;
use T3o\TerRest\Authentication\User\UnauthorizedUser;
use TYPO3\CMS\Core\Http\ServerRequest;
class AuthenticationDispatcherTest extends AbstractTestCase
{
/**
* @test
*/
public function executesBaseWithEmptyAuthenticationStackReturnsUnauthorizedApiUser(): void
{
$base = new class() implements AuthenticationHandlerInterface {
public function handle(ServerRequestInterface $request): ApiUserInterface
{
return new UnauthorizedUser();
}
};
$dispatcher = new AuthenticationDispatcher($base, new Container());
$apiUser = $dispatcher->handle(new ServerRequest());
self::assertSame('unauthorized', $apiUser->getName());
}
}
<?php
declare(strict_types = 1);
namespace T3o\TerRest\Tests\Unit\Authentication;
/*
* This file is part of TYPO3 CMS-extension "ter_rest", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
use Nimut\TestingFramework\TestCase\AbstractTestCase;
use T3o\TerRest\Authentication\Scope;
class ScopeTest extends AbstractTestCase
{
/**
* @test
*/
public function convertThrowsExceptionOnInvalidScope(): void
{
$this->expectExceptionCode(1602752145);
Scope::convert(['extension:read', 'extension:foo']);
}
/**
* @dataProvider scopeTestsDataProvider
* @test
*
* @param array $input
* @param int $expected
*/
public function convertTest(array $input, int $expected): void
{
self::assertEquals($expected, Scope::convert($input));
}
/**
* @dataProvider scopeTestsDataProvider
* @test
*
* @param array $input
* @param int $expected
*/
public function isGrantedTest(array $input, int $expected): void
{
$scope = new Scope(Scope::convert($input));
self::assertTrue($scope->isGranted($expected));
}
/**
* @test
*/
public function isControllerTest(): void
{
self::assertFalse((new Scope(Scope::EXTENSION_READ + Scope::EXTENSION_WRITE))->isController());
self::assertTrue((new Scope(Scope::EXTENSION_ADMIN))->isController());
}
/**
* @dataProvider stringRepresentationTestDataProvider
* @test
*
* @param array $input
* @param string $expected
*/
public function stringRepresentationTest(array $input, string $expected): void
{
$scope = new Scope(Scope::convert($input));
self::assertEquals($expected, (string)$scope);
}
/**
* @return \Generator
*/
public function scopeTestsDataProvider(): \Generator
{
yield 'No scope' => [
[],
Scope::EXTENSION_NONE
];
yield 'Read/Write' => [
['extension:read', 'extension:write'],
Scope::EXTENSION_READ + Scope::EXTENSION_WRITE
];
yield 'Array unique is appliead' => [
['extension:read', 'extension:write', 'extension:write'],
Scope::EXTENSION_READ + Scope::EXTENSION_WRITE
];
yield 'All scopes are given' => [
['extension:read', 'extension:write', 'extension:admin', 'extension:review'],
Scope::ALL
];
}
/**
* @return \Generator
*/
public function stringRepresentationTestDataProvider(): \Generator
{
yield 'No scope' => [
[],
''
];
yield 'Read/Write' => [
['extension:read', 'extension:write'],
'extension:read,extension:write'
];
yield 'Array unique is appliead' => [
['extension:read', 'extension:write', 'extension:write'],
'extension:read,extension:write'
];
yield 'Middle scope missing' => [
['extension:read', 'extension:review'],
'extension:read,extension:review'
];
yield 'All scopes are given' => [
['extension:read', 'extension:write', 'extension:admin', 'extension:review'],
'extension:read,extension:write,extension:admin,extension:review'
];
}
}
<?php
declare(strict_types = 1);
namespace T3o\TerRest\Tests\Unit\Http;
/*
* This file is part of TYPO3 CMS-extension "ter_rest", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
use Nimut\TestingFramework\TestCase\AbstractTestCase;
use Psr\Http\Message\ServerRequestInterface;
use T3o\TerRest\Exception\BadRequestException;
use T3o\TerRest\Exception\ClientErrorException;
use T3o\TerRest\Exception\ConflictException;
use T3o\TerRest\Exception\ForbiddenException;
use T3o\TerRest\Exception\InvalidCredentialsException;
use T3o\TerRest\Exception\InvalidJwtTokenException;
use T3o\TerRest\Exception\NotFoundException;
use T3o\TerRest\Exception\TokenMissingException;
use T3o\TerRest\Exception\UnauthorizedException;
use T3o\TerRest\Exception\UnsupportedMediaTypeException;
use T3o\TerRest\Http\ResponseFactory;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\ServerRequest;
class ResponseFactoryTest extends AbstractTestCase
{
protected ResponseFactory $subject;
protected ServerRequestInterface $request;
protected function setUp(): void
{
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['ter_rest']['routing']['responseTypes'] = [
'*/*' => JsonResponse::class,
'application/json' => JsonResponse::class
];
$this->subject = new ResponseFactory(new ExtensionConfiguration());
$this->request = new ServerRequest('https://some-uri.com/some/path');
}
protected function tearDown(): void
{
unset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['ter_rest']['routing']['responseTypes']);
}
/**
* @test
*/
public function createResponseForTypeFallsBackToJson(): void
{
static::assertInstanceOf(JsonResponse::class, $this->subject->createResponseForType('foo/bar'));
}
/**
* @test
*/
public function createResponseForRequestThrowsExceptionOnMissingHeader(): void
{
$this->expectExceptionCode(1601025839);
$this->subject->createResponseForRequest($this->request);
}
/**
* @test
*/
public function defaultCreateResponseForRequestTest(): void
{
$response = $this->subject->createResponseForRequest($this->request->withHeader('accept', '*/*'));
self::assertSame(200, $response->getStatusCode());
self::assertSame('', $response->getBody()->getContents());
self::assertTrue($response->hasHeader('x-ratelimit-limit'));
self::assertFalse($response->hasHeader('cache-control'));
}
/**
* @test
*/
public function createResponseForRequestTest(): void
{
$response = $this->subject->createResponseForRequest(
$this->request->withHeader('accept', '*/*'),
[
'some' => 'data',
'another' => [
'data'
]
],
206,
false,
false
);
self::assertSame(206, $response->getStatusCode());
self::assertSame('{"some":"data","another":["data"]}', $response->getBody()->getContents());
self::assertFalse($response->hasHeader('x-ratelimit-limit'));
self::assertTrue($response->hasHeader('cache-control'));
}
/**
* @test
*/
public function createTokenCreatedResponseTest(): void
{
$response = $this->subject->createTokenCreatedResponse(
$this->request->withHeader('accept', 'application/json'),
'accessJWT',
'refreshJWT',
12345,
'extension:admin',
''
);
self::assertSame(201, $response->getStatusCode());
self::assertSame('{"token_type":"bearer","access_token":"accessJWT","refresh_token":"refreshJWT","expires_in":12345,"scope":"extension:admin","extensions":""}', $response->getBody()->getContents());
self::assertTrue($response->hasHeader('x-ratelimit-limit'));
self::assertTrue($response->hasHeader('cache-control'));
}
/**
* @test
*/
public function createErrorResponseTest(): void
{
$response = $this->subject->createErrorResponse(
$this->request->withHeader('accept', 'application/json'),
123456789,
'some error message'
);
self::assertSame(500, $response->getStatusCode());
self::assertSame('{"status":500,"code":123456789,"message":"some error message"}', $response->getBody()->getContents());
self::assertTrue($response->hasHeader('x-ratelimit-limit'));
self::assertFalse($response->hasHeader('cache-control'));
}
/**
* @test
* @dataProvider createClientErrorResponseTestDataProvider
*
* @param ClientErrorException $exception
* @param int $expectedCode
* @param string $expectedData
* @param array $expectedHeaders
*/
public function createClientErrorResponseTest(
ClientErrorException $exception,
int $expectedCode,
string $expectedData,
array $expectedHeaders
): void {
$response = $this->subject->createClientErrorResponse(
$this->request->withHeader('accept', 'application/json'),
$exception
);
self::assertSame($expectedCode, $response->getStatusCode());
self::assertSame($expectedData, $response->getBody()->getContents());
foreach ($expectedHeaders as $name => $value) {
self::assertTrue($response->hasHeader($name));
self::assertEquals($value, $response->getHeader($name)[0]);
}
}
/**
* @return \Generator
*/
public function createClientErrorResponseTestDataProvider(): \Generator
{
yield 'Default client error' => [
new ClientErrorException('some client error', 12345),
400,
'{"status":400,"code":12345,"error":"","error_description":"some client error"}',
['x-ratelimit-limit' => 100, 'x-ratelimit-remaining' => 99]
];
yield 'Custom client error' => [
new ClientErrorException('custom client error', 54321, 'custom_client_request', 412),
412,
'{"status":412,"code":54321,"error":"custom_client_request","error_description":"custom client error"}',
['x-ratelimit-limit' => 100, 'x-ratelimit-remaining' => 99]
];
yield 'Bad request' => [
new BadRequestException('some bad request', 54321),
400,
'{"status":400,"code":54321,"error":"invalid_request","error_description":"some bad request"}',
['x-ratelimit-limit' => 100, 'x-ratelimit-remaining' => 99]
];
yield 'Unauthorized' => [
new UnauthorizedException('you are unauthorized', 12121),
401,
'{"status":401,"code":12121,"error":"unauthorized","error_description":"you are unauthorized"}',
[
'x-ratelimit-limit' => 100,
'x-ratelimit-remaining' => 99,
'www-authenticate' => 'Basic realm="Access to protected endpoint"'
]
];
yield 'Forbidden' => [
new ForbiddenException('that is forbidden', 12121),
403,
'{"status":403,"code":12121,"error":"access_denied","error_description":"that is forbidden"}',
['x-ratelimit-limit' => 100, 'x-ratelimit-remaining' => 99]
];
yield 'Not found' => [
new NotFoundException('Could not find the resource', 83456643),
404,
'{"status":404,"code":83456643,"error":"not_found","error_description":"Could not find the resource"}',
['x-ratelimit-limit' => 100, 'x-ratelimit-remaining' => 99]
];
yield 'Conflict' => [
new ConflictException('there is a conflict', 67463),
409,
'{"status":409,"code":67463,"error":"conflict","error_description":"there is a conflict"}',
['x-ratelimit-limit' => 100, 'x-ratelimit-remaining' => 99]
];
yield 'Unsupported media type' => [
new UnsupportedMediaTypeException('Can not process this media type', 917492),
415,
'{"status":415,"code":917492,"error":"unsupported","error_description":"Can not process this media type"}',
['x-ratelimit-limit' => 100, 'x-ratelimit-remaining' => 99]
];
yield 'Token missing' => [
new TokenMissingException('Your token is missing', 111111),
400,
'{"status":400,"code":111111,"error":"invalid_token","error_description":"Your token is missing"}',
['x-ratelimit-limit' => 100, 'x-ratelimit-remaining' => 99]
];
yield 'Invalid JWT token' => [
new InvalidJwtTokenException('Your token is invalid', 12122121),
400,
'{"status":400,"code":12122121,"error":"invalid_token","error_description":"Your token is invalid"}',
['x-ratelimit-limit' => 100, 'x-ratelimit-remaining' => 99]
];
yield 'Invalid credentials' => [
new InvalidCredentialsException('Your credentials are invalid', 222222),
401,
'{"status":401,"code":222222,"error":"invalid_credentials","error_description":"Your credentials are invalid"}',
[
'x-ratelimit-limit' => 100,
'x-ratelimit-remaining' => 99,
'www-authenticate' => 'Basic realm="Access to protected endpoint"'
]
];
}
}
<?php
declare(strict_types = 1);
namespace T3o\TerRest\Tests\Unit\Routing\RouteArgument;
/*
* This file is part of TYPO3 CMS-extension "ter_rest", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
use Nimut\TestingFramework\TestCase\AbstractTestCase;
use T3o\TerRest\Routing\RouteArgument\ArrayArgument;
use T3o\TerRest\Routing\RouteArgument\DataStructureRouteArgumentInterface;
use T3o\TerRest\Routing\RouteArgument\IntegerArgument;
use T3o\TerRest\Routing\RouteArgument\ObjectArgument;
use T3o\TerRest\Routing\RouteArgument\RouteArgumentFactory;
use T3o\TerRest\Routing\RouteArgument\StringArgument;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
class RouteArgumentFactoryTest extends AbstractTestCase
{
protected RouteArgumentFactory $routeArgumentFactory;
protected function setUp(): void
{
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['ter_rest']['routing']['routeArguments'] = [
'String' => StringArgument::class,
'Integer' => IntegerArgument::class,
'Object' => ObjectArgument::class,
'Array' => ArrayArgument::class
];
$this->routeArgumentFactory = new RouteArgumentFactory(new ExtensionConfiguration());
}
protected function tearDown(): void
{
unset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['ter_rest']['routing']['routeArguments']);
}
/**
* @test
*/
public function throwsExcpetionOnEmptyType(): void
{
$this->expectExceptionCode(1601024733);
$this->routeArgumentFactory->create('foo', [], '');
}
/**
* @test
*/
public function throwsExcpetionOnInvalidType(): void
{
$this->expectExceptionCode(1601024789);
$this->routeArgumentFactory->create('foo', ['schema' => ['type' => 'blob']], '');
}
/**
* @test
* @dataProvider createRouteArgumentTestDataProvider
*
* @param array $input
* @param string $expected
* @param int $expectionCode
*/
public function createRouteArgumentTest(array $input, string $expected, int $expectionCode): void
{
if ($expectionCode) {
$this->expectExceptionCode($expectionCode);
}
$routeArgument = $this->routeArgumentFactory->create($input['name'], $input['configuration'], $input['value']);
if (!$expectionCode) {
$result = '';
if ($routeArgument instanceof DataStructureRouteArgumentInterface) {
$routeArgumentStore = $routeArgument->getProperties();
foreach ($routeArgumentStore->getProperties() as $property) {
$property->isValid();
$result .= json_encode($property);
}
} else {
$routeArgument->isValid();
$result = json_encode($routeArgument);
}
self::assertEquals($expected, $result);
}
}
/**
* @return \Generator
*/
public function createRouteArgumentTestDataProvider(): \Generator
{
yield 'StringArgument Valid' => [
$this->getStringArgumentConfiguration(['value' => 'some_value']),
'{"name":"key","value":"some_value","validationErrors":[]}',
0
];
yield 'StringArgument Invalid (does not match pattern and too long)' => [
$this->getStringArgumentConfiguration(['value' => '12345foobar']),
'{"name":"key","value":"12345foobar","validationErrors":["The given value \'12345foobar\' for argument \'key\''
. ' must be at max 10 chars long.","The given value \'12345foobar\' for argument \'key\' does not match the pattern:'
. ' ^[a-z]{1}[a-z0-9_]+$"]}',
0
];
yield 'StringArgument Invalid (wrong type)' => [
$this->getStringArgumentConfiguration(['value' => 123]),
'',
1601313573
];
yield 'IntegerArgument Valid' => [
$this->getIntegerArgumentConfiguration(['value' => 0]),
'{"name":"review_state","value":0,"validationErrors":[]}',
0
];
yield 'IntegerArgument Invalid (does not match enum)' => [
$this->getIntegerArgumentConfiguration(['value' => 1]),
'{"name":"review_state","value":1,"validationErrors":["The given value \'1\' for argument \'review_state\''
. ' is not allowed. Allowed values: -2,-1,0"]}',
0
];
yield 'IntegerArgument Invalid (wrong type)' => [
$this->getIntegerArgumentConfiguration(['value' => 'some string']),
'',
1601313518
];
yield 'ArrayArgument Valid' => [
$this->getArrayArgumentConfiguration(['value' => 'foo,bar']),
'{"name":"scope[0]","value":"foo","validationErrors":[]}{"name":"scope[1]","value":"bar","validationErrors":