1
0
mirror of synced 2024-11-24 22:06:06 +03:00

Initial service bundle (#1)

This commit is contained in:
Akolzin Dmitry 2021-02-05 14:47:54 +03:00 committed by GitHub
parent 9309a2d122
commit d44da0d531
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1612 additions and 0 deletions

34
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: ci
on:
push:
branches:
- '**'
tags-ignore:
- '*.*'
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['7.3', '7.4', '8.0']
steps:
- uses: actions/checkout@v2
- name: Setup PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
coverage: pcov
- name: Composer cache
uses: actions/cache@v2
with:
path: ${{ env.HOME }}/.composer/cache
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install -o
- name: Run tests
run: composer run tests
- name: Coverage
run: bash <(curl -s https://codecov.io/bash)

6
.gitignore vendored
View File

@ -30,6 +30,7 @@
!bin/console !bin/console
!bin/symfony_requirements !bin/symfony_requirements
/vendor/ /vendor/
composer.lock
# Assets and user uploads # Assets and user uploads
/web/bundles/ /web/bundles/
@ -38,6 +39,8 @@
# PHPUnit # PHPUnit
/app/phpunit.xml /app/phpunit.xml
/phpunit.xml /phpunit.xml
.phpunit.result.cache
test-report.xml
# Build data # Build data
/build/ /build/
@ -50,3 +53,6 @@
# Embedded web-server pid file # Embedded web-server pid file
/.web-server-pid /.web-server-pid
.idea
coverage.xml

View File

@ -0,0 +1,43 @@
<?php
namespace RetailCrm\ServiceBundle\ArgumentResolver;
use RetailCrm\ServiceBundle\Exceptions\InvalidRequestArgumentException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* Class AbstractValueResolver
*
* @package RetailCrm\ServiceBundle\ArgumentResolver
*/
abstract class AbstractValueResolver
{
protected $validator;
/**
* AbstractValueResolver constructor.
*
* @param ValidatorInterface $validator
*/
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
/**
* @param object $data
*
* @return void
*/
protected function validate(object $data): void
{
$errors = $this->validator->validate($data);
if (0 !== count($errors)) {
throw new InvalidRequestArgumentException(
sprintf("Invalid request parameter %s", \get_class($data)),
400,
$errors
);
}
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace RetailCrm\ServiceBundle\ArgumentResolver;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\Serializer\SerializerInterface;
use Generator;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* Class CallbackValueResolver
*
* @package RetailCrm\ServiceBundle\ArgumentResolver
*/
class CallbackValueResolver extends AbstractValueResolver implements ArgumentValueResolverInterface
{
private $serializer;
private $requestSchema;
/**
* CallbackValueResolver constructor.
*
* @param SerializerInterface $serializer
* @param ValidatorInterface $validator
* @param array $requestSchema
*/
public function __construct(
SerializerInterface $serializer,
ValidatorInterface $validator,
array $requestSchema
) {
parent::__construct($validator);
$this->serializer = $serializer;
$this->requestSchema = $requestSchema;
}
/**
* {@inheritdoc }
*/
public function supports(Request $request, ArgumentMetadata $argument): bool
{
if (empty($this->requestSchema) || $request->getMethod() !== Request::METHOD_POST) {
return false;
}
return null !== $this->search($request, $argument);
}
/**
* {@inheritdoc }
*/
public function resolve(Request $request, ArgumentMetadata $argument): Generator
{
$parameter = $this->search($request, $argument);
$data = $this->serializer->deserialize($request->request->get($parameter), $argument->getType(), 'json');
$this->validate($data);
yield $data;
}
/**
* @param Request $request
* @param ArgumentMetadata $argument
*
* @return string|null
*/
private function search(Request $request, ArgumentMetadata $argument): ?string
{
foreach ($this->requestSchema as $callback) {
if ($argument->getType() !== $callback['type']) {
continue;
}
foreach ($callback['params'] as $param) {
if ($request->request->has($param)) {
return $param;
}
}
}
return null;
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace RetailCrm\ServiceBundle\ArgumentResolver;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Generator;
/**
* Class ClientValueResolver
*
* @package RetailCrm\ServiceBundle\ArgumentResolver
*/
class ClientValueResolver extends AbstractValueResolver implements ArgumentValueResolverInterface
{
private $serializer;
private $denormalizer;
private $requestSchema;
/**
* ClientValueResolver constructor.
*
* @param ValidatorInterface $validator
* @param SerializerInterface $serializer
* @param DenormalizerInterface $denormalizer
* @param array $requestSchema
*/
public function __construct(
ValidatorInterface $validator,
SerializerInterface $serializer,
DenormalizerInterface $denormalizer,
array $requestSchema
) {
parent::__construct($validator);
$this->serializer = $serializer;
$this->denormalizer = $denormalizer;
$this->requestSchema = $requestSchema;
}
/**
* {@inheritdoc}
*/
public function supports(Request $request, ArgumentMetadata $argument): bool
{
return in_array($argument->getType(), $this->requestSchema, true);
}
/**
* {@inheritdoc}
*/
public function resolve(Request $request, ArgumentMetadata $argument): Generator
{
if (Request::METHOD_GET === $request->getMethod()) {
$dto = $this->handleGetData($request->query->all(), $argument->getType());
} else {
$dto = $this->handlePostData($request->getContent(), $argument->getType());
}
$this->validate($dto);
yield $dto;
}
/**
* @param array $data
* @param string $type
*
* @return object
*
* @throws ExceptionInterface
*/
private function handleGetData(array $data, string $type): object
{
return $this->denormalizer->denormalize($data, $type);
}
/**
* @param string $data
* @param string $type
*
* @return object
*/
private function handlePostData(string $data, string $type): object
{
return $this->serializer->deserialize($data, $type, 'json');
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace RetailCrm\ServiceBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* Class Configuration
*
* @package RetailCrm\ServiceBundle\DependencyInjection
*/
class Configuration implements ConfigurationInterface
{
/**
* {@inheritdoc }
*/
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('retail_crm_service');
$rootNode = $treeBuilder->getRootNode();
$rootNode
->children()
->arrayNode('request_schema')
->children()
->arrayNode('callback')
->arrayPrototype()
->children()
->scalarNode('type')->isRequired()->end()
->arrayNode('params')
->isRequired()->scalarPrototype()->end()
->end()
->end()
->end()
->end()
->arrayNode('client')
->scalarPrototype()->end()
->end()
->end()
->end()
->end();
return $treeBuilder;
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace RetailCrm\ServiceBundle\DependencyInjection;
use RetailCrm\ServiceBundle\ArgumentResolver\CallbackValueResolver;
use RetailCrm\ServiceBundle\ArgumentResolver\ClientValueResolver;
use RetailCrm\ServiceBundle\Response\ErrorJsonResponseFactory;
use RetailCrm\ServiceBundle\Security\CallbackClientAuthenticator;
use RetailCrm\ServiceBundle\Security\FrontApiClientAuthenticator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
/**
* Class RetailCrmServiceExtension
*
* @package RetailCrm\ServiceBundle\DependencyInjection
*/
class RetailCrmServiceExtension extends Extension
{
/**
* @param array $configs
* @param ContainerBuilder $container
*/
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter(
'retail_crm_service.request_schema.callback',
$config['request_schema']['callback']
);
$container->setParameter(
'retail_crm_service.request_schema.client',
$config['request_schema']['client']
);
$container
->register(CallbackValueResolver::class)
->setArgument('$requestSchema', '%retail_crm_service.request_schema.callback%')
->addTag('controller.argument_value_resolver', ['priority' => 50])
->setAutowired(true);
$container
->register(ClientValueResolver::class)
->setArgument('$requestSchema', '%retail_crm_service.request_schema.client%')
->addTag('controller.argument_value_resolver', ['priority' => 50])
->setAutowired(true);
$container
->register(ErrorJsonResponseFactory::class)
->setAutowired(true);
$container
->register(CallbackClientAuthenticator::class)
->setAutowired(true);
$container
->register(FrontApiClientAuthenticator::class)
->setAutowired(true);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace RetailCrm\ServiceBundle\Exceptions;
use InvalidArgumentException;
use Throwable;
/**
* Class InvalidRequestArgumentException
*
* @package RetailCrm\ServiceBundle\Exceptions
*/
class InvalidRequestArgumentException extends InvalidArgumentException
{
private $validateErrors;
/**
* InvalidRequestArgumentException constructor.
* @param string $message
* @param int $code
* @param array $errors
* @param Throwable|null $previous
*/
public function __construct($message = "", $code = 0, iterable $errors = [], Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->validateErrors = $errors;
}
/**
* @return iterable
*/
public function getValidateErrors(): iterable
{
return $this->validateErrors;
}
}

26
Models/Error.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace RetailCrm\ServiceBundle\Models;
/**
* Class Error
*
* @package RetailCrm\ServiceBundle\Models
*/
class Error
{
/**
* @var string
*/
public $code;
/**
* @var string
*/
public $message;
/**
* @var array
*/
public $details;
}

148
Resources/doc/index.md Normal file
View File

@ -0,0 +1,148 @@
## Installation
`composer require retailcrm/service-bundle`
## Usage
Enable bundle in `config/bundles.php`:
```php
<?php
return [
// other bundles
RetailCrm\ServiceBundle\RetailCrmServiceBundle::class => ['all' => true]
];
```
Create bundle config file in `config/packages/retail_crm_service.yaml`:
```yaml
retail_crm_service:
request_schema: ~
```
### Deserializing incoming requests
#### Callbacks (form data)
To automatically get the callback request parameter
```php
class AppController extends AbstractController
{
public function activityAction(\App\Dto\Callback\Activity $activity): Response
{
// handle activity
}
}
```
add to the config:
```yaml
retail_crm_service:
request_schema:
callback:
- type: App\Dto\Callback\Activity
params: ["activity"]
```
request automatically will be deserialization to $activity.
#### Body json content
```php
class AppController extends AbstractController
{
public function someAction(\App\Dto\Body $activity): Response
{
// handle activity
}
}
```
add to the config:
```yaml
retail_crm_service:
request_schema:
client:
- App\Dto\Body
```
### Authentication
Example security configuration:
```yaml
security:
providers:
client:
entity:
class: 'App\Entity\Connection' # must implements UserInterface
property: 'clientId'
firewalls:
api:
pattern: ^/api
provider: client
anonymous: ~
lazy: true
stateless: false
guard:
authenticators:
- RetailCrm\ServiceBundle\Security\FrontApiClientAuthenticator
callback:
pattern: ^/callback
provider: client
anonymous: ~
lazy: true
stateless: true
guard:
authenticators:
- RetailCrm\ServiceBundle\Security\CallbackClientAuthenticator
main:
anonymous: true
lazy: true
access_control:
- { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } # login for programmatically authentication user
- { path: ^/api, roles: ROLE_USER }
- { path: ^/callback, roles: ROLE_USER }
```
To authenticate the user after creating it, you can use the following code
```php
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use RetailCrm\ServiceBundle\Security\FrontApiClientAuthenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class AppController extends AbstractController
{
public function someAction(
Request $request,
GuardAuthenticatorHandler $guardAuthenticatorHandler,
FrontApiClientAuthenticator $frontApiClientAuthenticator,
ConnectionManager $manager
): Response {
$user = $manager->getUser(); // getting user
$guardAuthenticatorHandler->authenticateUserAndHandleSuccess(
$user,
$request,
$frontApiClientAuthenticator,
'api'
);
// ...
}
}
```

View File

@ -0,0 +1,44 @@
<?php
namespace RetailCrm\ServiceBundle\Response;
use RetailCrm\ServiceBundle\Models\Error;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Class ErrorJsonResponseFactory
*
* @package RetailCrm\ServiceBundle\Response
*/
class ErrorJsonResponseFactory
{
private $serializer;
/**
* ErrorJsonResponseFactory constructor.
*
* @param SerializerInterface $serializer
*/
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
/**
* @param Error $error
* @param int $statusCode
* @param array $headers
*
* @return Response
*/
public function create(Error $error, int $statusCode = Response::HTTP_BAD_REQUEST, array $headers = []): Response
{
return JsonResponse::fromJsonString(
$this->serializer->serialize($error, 'json'),
$statusCode,
$headers
);
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace RetailCrm\ServiceBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class RetailCrmServiceBundle extends Bundle
{
}

View File

@ -0,0 +1,89 @@
<?php
namespace RetailCrm\ServiceBundle\Security;
use RetailCrm\ServiceBundle\Models\Error;
use RetailCrm\ServiceBundle\Response\ErrorJsonResponseFactory;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
/**
* Class AbstractClientAuthenticator
*
* @package RetailCrm\ServiceBundle\Security
*/
abstract class AbstractClientAuthenticator extends AbstractGuardAuthenticator
{
public const AUTH_FIELD = 'clientId';
private $errorResponseFactory;
/**
* AbstractClientAuthenticator constructor.
*
* @param ErrorJsonResponseFactory $errorResponseFactory
*/
public function __construct(ErrorJsonResponseFactory $errorResponseFactory)
{
$this->errorResponseFactory = $errorResponseFactory;
}
/**
* {@inheritdoc }
*/
public function start(Request $request, AuthenticationException $authException = null): Response
{
$error = new Error();
$error->message = 'Authentication required';
return $this->errorResponseFactory->create($error,Response::HTTP_UNAUTHORIZED);
}
/**
* {@inheritdoc }
*/
public function getCredentials(Request $request): string
{
return $request->get(static::AUTH_FIELD);
}
/**
* {@inheritdoc }
*/
public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
{
return $userProvider->loadUserByUsername($credentials);
}
/**
* {@inheritdoc }
*/
public function checkCredentials($credentials, UserInterface $user): bool
{
return true;
}
/**
* {@inheritdoc }
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$error = new Error();
$error->message = $exception->getMessageKey();
return $this->errorResponseFactory->create($error,Response::HTTP_FORBIDDEN);
}
/**
* {@inheritdoc }
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
{
return null;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace RetailCrm\ServiceBundle\Security;
use Symfony\Component\HttpFoundation\Request;
/**
* Class CallbackClientAuthenticator
*
* @package RetailCrm\ServiceBundle\Security
*/
class CallbackClientAuthenticator extends AbstractClientAuthenticator
{
/**
* {@inheritdoc }
*/
public function supports(Request $request): bool
{
return $request->request->has(static::AUTH_FIELD) || $request->query->has(static::AUTH_FIELD);
}
/**
* {@inheritdoc }
*/
public function supportsRememberMe(): bool
{
return false;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace RetailCrm\ServiceBundle\Security;
use RetailCrm\ServiceBundle\Response\ErrorJsonResponseFactory;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
/**
* Class FrontApiClientAuthenticator
*
* @package RetailCrm\ServiceBundle\Security
*/
class FrontApiClientAuthenticator extends AbstractClientAuthenticator
{
private $security;
/**
* FrontApiClientAuthenticator constructor.
*
* @param ErrorJsonResponseFactory $errorResponseFactory
* @param Security $security
*/
public function __construct(
ErrorJsonResponseFactory $errorResponseFactory,
Security $security
) {
parent::__construct($errorResponseFactory);
$this->security = $security;
}
/**
* {@inheritdoc }
*/
public function supports(Request $request): bool
{
if ($this->security->getUser()) {
return false;
}
return $request->request->has(static::AUTH_FIELD);
}
/**
* {@inheritdoc }
*/
public function supportsRememberMe(): bool
{
return true;
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace RetailCrm\ServiceBundle\Tests\ArgumentResolver;
use PHPUnit\Framework\TestCase;
use RetailCrm\ServiceBundle\ArgumentResolver\CallbackValueResolver;
use RetailCrm\ServiceBundle\Exceptions\InvalidRequestArgumentException;
use RetailCrm\ServiceBundle\Tests\DataFixtures\RequestDto;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validation;
use Generator;
/**
* Class CallbackValueResolverTest
*
* @package RetailCrm\ServiceBundle\Tests\ArgumentResolver
*/
class CallbackValueResolverTest extends TestCase
{
private $resolver;
public function setUp(): void
{
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
$this->resolver = new CallbackValueResolver(
$serializer,
Validation::createValidatorBuilder()
->enableAnnotationMapping()
->getValidator(),
[
[
'type' => RequestDto::class,
'params' => ['request_parameter']
]
]
);
}
public function testSupports(): void
{
$argument = new ArgumentMetadata('RequestDto', RequestDto::class, false, false, null);
$request = new Request(
[],
['request_parameter' => json_encode(['param' => 'parameter'], JSON_THROW_ON_ERROR)],
[],
[],
[],
['REQUEST_METHOD' => Request::METHOD_POST]
);
$result = $this->resolver->supports($request, $argument);
static::assertTrue($result);
}
public function testNotSupports(): void
{
$argument = new ArgumentMetadata('RequestDto', 'NotFoundRequestDto', false, false, null);
$request = new Request(
[],
['request_parameter' => json_encode(['param' => 'parameter'], JSON_THROW_ON_ERROR)],
[],
[],
[],
['REQUEST_METHOD' => Request::METHOD_POST]
);
$result = $this->resolver->supports($request, $argument);
static::assertFalse($result);
}
public function testResolve(): void
{
$argument = new ArgumentMetadata('RequestDto', RequestDto::class, false, false, null);
$request = new Request(
[],
['request_parameter' => json_encode(['param' => 'parameter'], JSON_THROW_ON_ERROR)],
[],
[],
[],
['REQUEST_METHOD' => Request::METHOD_POST]
);
$result = $this->resolver->resolve($request, $argument);
static::assertInstanceOf(Generator::class, $result);
static::assertInstanceOf(RequestDto::class, $result->current());
static::assertEquals('parameter', $result->current()->param);
}
public function testResolveFailure(): void
{
$argument = new ArgumentMetadata('RequestDto', RequestDto::class, false, false, null);
$request = new Request(
[],
['request_parameter' => json_encode(['param' => null], JSON_THROW_ON_ERROR)],
[],
[],
[],
['REQUEST_METHOD' => Request::METHOD_POST]
);
$this->expectException(InvalidRequestArgumentException::class);
$result = $this->resolver->resolve($request, $argument);
$result->current();
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace RetailCrm\ServiceBundle\Tests\ArgumentResolver;
use PHPUnit\Framework\TestCase;
use RetailCrm\ServiceBundle\ArgumentResolver\ClientValueResolver;
use RetailCrm\ServiceBundle\Exceptions\InvalidRequestArgumentException;
use RetailCrm\ServiceBundle\Tests\DataFixtures\RequestDto;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validation;
use Generator;
/**
* Class ClientValueResolverTest
*
* @package RetailCrm\ServiceBundle\Tests\ArgumentResolver
*/
class ClientValueResolverTest extends TestCase
{
private $resolver;
public function setUp(): void
{
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
$this->resolver = new ClientValueResolver(
Validation::createValidatorBuilder()
->enableAnnotationMapping()
->getValidator(),
$serializer,
$serializer,
[
RequestDto::class
]
);
}
public function testSupports(): void
{
$argument = new ArgumentMetadata('RequestDto', RequestDto::class, false, false, null);
$request = new Request();
$result = $this->resolver->supports($request, $argument);
static::assertTrue($result);
}
public function testNotSupports(): void
{
$argument = new ArgumentMetadata('RequestDto', 'NotFoundRequestDto', false, false, null);
$request = new Request();
$result = $this->resolver->supports($request, $argument);
static::assertFalse($result);
}
public function testResolvePost(): void
{
$argument = new ArgumentMetadata('RequestDto', RequestDto::class, false, false, null);
$request = new Request(
[],
[],
[],
[],
[],
['REQUEST_METHOD' => Request::METHOD_POST],
json_encode(['param' => 'parameter'], JSON_THROW_ON_ERROR)
);
$result = $this->resolver->resolve($request, $argument);
static::assertInstanceOf(Generator::class, $result);
static::assertInstanceOf(RequestDto::class, $result->current());
static::assertEquals('parameter', $result->current()->param);
}
public function testResolvePostFailure(): void
{
$argument = new ArgumentMetadata('RequestDto', RequestDto::class, false, false, null);
$request = new Request(
[],
[],
[],
[],
[],
['REQUEST_METHOD' => Request::METHOD_POST],
json_encode(['param' => null], JSON_THROW_ON_ERROR)
);
$this->expectException(InvalidRequestArgumentException::class);
$result = $this->resolver->resolve($request, $argument);
$result->current();
}
public function testResolveGet(): void
{
$argument = new ArgumentMetadata('RequestDto', RequestDto::class, false, false, null);
$request = new Request(
['param' => 'parameter'],
[],
[],
[],
[],
[]
);
$result = $this->resolver->resolve($request, $argument);
static::assertInstanceOf(Generator::class, $result);
static::assertInstanceOf(RequestDto::class, $result->current());
static::assertEquals('parameter', $result->current()->param);
}
public function testResolveGetFailure(): void
{
$argument = new ArgumentMetadata('RequestDto', RequestDto::class, false, false, null);
$request = new Request(
['param' => null],
[],
[],
[],
[],
[]
);
$this->expectException(InvalidRequestArgumentException::class);
$result = $this->resolver->resolve($request, $argument);
$result->current();
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace RetailCrm\ServiceBundle\Tests\DataFixtures;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Class RequestDto
*
* @package RetailCrm\ServiceBundle\Tests\DataFixtures
*/
class RequestDto
{
/**
* @var string
* @Assert\NotNull()
*/
public $param;
}

View File

@ -0,0 +1,37 @@
<?php
namespace RetailCrm\ServiceBundle\Tests\DataFixtures;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Class User
*
* @package RetailCrm\ServiceBundle\Tests\DataFixtures
*/
class User implements UserInterface
{
public function getRoles(): array
{
return ["USER"];
}
public function getPassword(): string
{
return "123";
}
public function getSalt(): string
{
return "salt";
}
public function getUsername(): string
{
return "user";
}
public function eraseCredentials(): void
{
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace RetailCrm\ServiceBundle\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use RetailCrm\ServiceBundle\DependencyInjection\Configuration;
use Symfony\Component\Config\Definition\Processor;
/**
* Class ConfigurationTest
*
* @package RetailCrm\ServiceBundle\Tests\DependencyInjection
*/
class ConfigurationTest extends TestCase
{
public function testConfig(): void
{
$processor = new Processor();
$configs = [
[
'request_schema' => [
'callback' => [
[
'type' => 'type',
'params' => ['param']
]
],
'client' => [
'type1',
'type2'
]
]
]
];
$config = $processor->processConfiguration(new Configuration(), $configs);
static::assertArrayHasKey('request_schema', $config);
static::assertArrayHasKey('callback', $config['request_schema']);
static::assertArrayHasKey('client', $config['request_schema']);
static::assertEquals(
[
'type' => 'type',
'params' => ['param']
],
$config['request_schema']['callback'][0]
);
static::assertEquals(
[
'type1',
'type2'
],
$config['request_schema']['client']
);
}
public function testPartConfig(): void
{
$processor = new Processor();
$configs = [
[
'request_schema' => [
'client' => [
'type',
]
]
]
];
$config = $processor->processConfiguration(new Configuration(), $configs);
static::assertArrayHasKey('client', $config['request_schema']);
static::assertEquals(['type'], $config['request_schema']['client']);
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace RetailCrm\ServiceBundle\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use RetailCrm\ServiceBundle\ArgumentResolver\CallbackValueResolver;
use RetailCrm\ServiceBundle\ArgumentResolver\ClientValueResolver;
use RetailCrm\ServiceBundle\DependencyInjection\RetailCrmServiceExtension;
use RetailCrm\ServiceBundle\Response\ErrorJsonResponseFactory;
use RetailCrm\ServiceBundle\Security\CallbackClientAuthenticator;
use RetailCrm\ServiceBundle\Security\FrontApiClientAuthenticator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
/**
* Class RetailCrmServiceExtensionTest
*
* @package RetailCrm\ServiceBundle\Tests\DependencyInjection
*/
class RetailCrmServiceExtensionTest extends TestCase
{
private $container;
protected function setUp(): void
{
$container = new ContainerBuilder(new EnvPlaceholderParameterBag());
$container->getCompilerPassConfig()->setOptimizationPasses([]);
$container->getCompilerPassConfig()->setRemovingPasses([]);
$extension = new RetailCrmServiceExtension();
$extension->load(
[
[
'request_schema' => []
]
],
$container
);
$container->compile();
$this->container = $container;
}
public function testLoad(): void
{
static::assertTrue($this->container->hasParameter('retail_crm_service.request_schema.callback'));
static::assertTrue($this->container->hasParameter('retail_crm_service.request_schema.client'));
static::assertTrue($this->container->hasDefinition(CallbackValueResolver::class));
static::assertTrue($this->container->hasDefinition(ClientValueResolver::class));
static::assertTrue($this->container->hasDefinition(ErrorJsonResponseFactory::class));
static::assertTrue($this->container->hasDefinition(CallbackClientAuthenticator::class));
static::assertTrue($this->container->hasDefinition(FrontApiClientAuthenticator::class));
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace RetailCrm\ServiceBundle\Tests\Response;
use PHPUnit\Framework\TestCase;
use RetailCrm\ServiceBundle\Models\Error;
use RetailCrm\ServiceBundle\Response\ErrorJsonResponseFactory;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class ErrorJsonResponseFactoryTest extends TestCase
{
private $serializer;
protected function setUp(): void
{
$this->serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
}
public function testCreate(): void
{
$factory = new ErrorJsonResponseFactory($this->serializer);
$error = new Error();
$error->message = 'Test error message';
$result = $factory->create($error);
static::assertInstanceOf(JsonResponse::class, $result);
static::assertEquals(Response::HTTP_BAD_REQUEST, $result->getStatusCode());
static::assertEquals('{"code":null,"message":"Test error message","details":null}', $result->getContent());
}
}

View File

@ -0,0 +1,135 @@
<?php
namespace RetailCrm\ServiceBundle\Tests\Security;
use PHPUnit\Framework\TestCase;
use RetailCrm\ServiceBundle\Response\ErrorJsonResponseFactory;
use RetailCrm\ServiceBundle\Security\CallbackClientAuthenticator;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Class CallbackClientAuthenticatorTest
*
* @package RetailCrm\ServiceBundle\Tests\Security
*/
class CallbackClientAuthenticatorTest extends TestCase
{
public function testStart(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$errorResponseFactory
->expects(static::once())
->method('create')
->willReturn(
new JsonResponse(['message' => 'Authentication required'], Response::HTTP_UNAUTHORIZED)
);
$auth = new CallbackClientAuthenticator($errorResponseFactory);
$result = $auth->start(new Request(), new AuthenticationException());
static::assertInstanceOf(JsonResponse::class, $result);
static::assertEquals(Response::HTTP_UNAUTHORIZED, $result->getStatusCode());
}
public function testGetCredentials(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$auth = new CallbackClientAuthenticator($errorResponseFactory);
$result = $auth->getCredentials(new Request([], [CallbackClientAuthenticator::AUTH_FIELD => '123']));
static::assertEquals('123', $result);
$result = $auth->getCredentials(new Request([CallbackClientAuthenticator::AUTH_FIELD => '123']));
static::assertEquals('123', $result);
}
public function testCheckCredentials(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$user = new class implements UserInterface {
public function getRoles(): array
{
return ["USER"];
}
public function getPassword(): string
{
return "123";
}
public function getSalt(): string
{
return "salt";
}
public function getUsername(): string
{
return "user";
}
public function eraseCredentials(): void
{
}
};
$auth = new CallbackClientAuthenticator($errorResponseFactory);
$result = $auth->checkCredentials(new Request(), $user);
static::assertTrue($result);
}
public function testOnAuthenticationFailure(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$errorResponseFactory
->expects(static::once())
->method('create')
->willReturn(
new JsonResponse(
['message' => 'An authentication exception occurred.'],
Response::HTTP_FORBIDDEN
)
);
$auth = new CallbackClientAuthenticator($errorResponseFactory);
$result = $auth->start(new Request(), new AuthenticationException());
static::assertInstanceOf(JsonResponse::class, $result);
static::assertEquals(Response::HTTP_FORBIDDEN, $result->getStatusCode());
}
public function testSupports(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$auth = new CallbackClientAuthenticator($errorResponseFactory);
$result = $auth->supports(new Request([], [CallbackClientAuthenticator::AUTH_FIELD => '123']));
static::assertTrue($result);
$result = $auth->supports(new Request([CallbackClientAuthenticator::AUTH_FIELD => '123']));
static::assertTrue($result);
$result = $auth->supports(new Request());
static::assertFalse($result);
}
public function testSupportsRememberMe(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$auth = new CallbackClientAuthenticator($errorResponseFactory);
$result = $auth->supportsRememberMe();
static::assertFalse($result);
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace RetailCrm\ServiceBundle\Tests\Security;
use PHPUnit\Framework\TestCase;
use RetailCrm\ServiceBundle\Response\ErrorJsonResponseFactory;
use RetailCrm\ServiceBundle\Security\CallbackClientAuthenticator;
use RetailCrm\ServiceBundle\Security\FrontApiClientAuthenticator;
use RetailCrm\ServiceBundle\Tests\DataFixtures\User;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Class FrontApiClientAuthenticatorTest
*
* @package RetailCrm\ServiceBundle\Tests\Security
*/
class FrontApiClientAuthenticatorTest extends TestCase
{
public function testStart(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$errorResponseFactory
->expects(static::once())
->method('create')
->willReturn(
new JsonResponse(['message' => 'Authentication required'], Response::HTTP_UNAUTHORIZED)
);
$security = $this->createMock(Security::class);
$auth = new FrontApiClientAuthenticator($errorResponseFactory, $security);
$result = $auth->start(new Request(), new AuthenticationException());
static::assertInstanceOf(JsonResponse::class, $result);
static::assertEquals(Response::HTTP_UNAUTHORIZED, $result->getStatusCode());
}
public function testGetCredentials(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$security = $this->createMock(Security::class);
$auth = new FrontApiClientAuthenticator($errorResponseFactory, $security);
$result = $auth->getCredentials(new Request([], [CallbackClientAuthenticator::AUTH_FIELD => '123']));
static::assertEquals('123', $result);
$result = $auth->getCredentials(new Request([CallbackClientAuthenticator::AUTH_FIELD => '123']));
static::assertEquals('123', $result);
}
public function testCheckCredentials(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$security = $this->createMock(Security::class);
$auth = new FrontApiClientAuthenticator($errorResponseFactory, $security);
$result = $auth->checkCredentials(new Request(), new User());
static::assertTrue($result);
}
public function testOnAuthenticationFailure(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$errorResponseFactory
->expects(static::once())
->method('create')
->willReturn(
new JsonResponse(
['message' => 'An authentication exception occurred.'],
Response::HTTP_FORBIDDEN
)
);
$security = $this->createMock(Security::class);
$auth = new FrontApiClientAuthenticator($errorResponseFactory, $security);
$result = $auth->start(new Request(), new AuthenticationException());
static::assertInstanceOf(JsonResponse::class, $result);
static::assertEquals(Response::HTTP_FORBIDDEN, $result->getStatusCode());
}
public function testSupportsFalse(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$security = $this->createMock(Security::class);
$security->method('getUser')->willReturn(new User());
$auth = new FrontApiClientAuthenticator($errorResponseFactory, $security);
$result = $auth->supports(new Request());
static::assertFalse($result);
}
public function testSupportsTrue(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$security = $this->createMock(Security::class);
$security->method('getUser')->willReturn(null);
$auth = new FrontApiClientAuthenticator($errorResponseFactory, $security);
$result = $auth->supports(new Request([], [FrontApiClientAuthenticator::AUTH_FIELD => '123']));
static::assertTrue($result);
}
public function testSupportsRememberMe(): void
{
$errorResponseFactory = $this->createMock(ErrorJsonResponseFactory::class);
$security = $this->createMock(Security::class);
$auth = new FrontApiClientAuthenticator($errorResponseFactory, $security);
$result = $auth->supportsRememberMe();
static::assertTrue($result);
}
}

43
composer.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "retailcrm/service-bundle",
"description": "Core bundle for RetailCRM integration services",
"type": "symfony-bundle",
"license": "MIT",
"authors": [
{
"name": "RetailCRM",
"email": "support@retailcrm.pro"
}
],
"minimum-stability": "stable",
"require": {
"php": ">=7.3",
"symfony/framework-bundle": "^4.0|^5.0",
"symfony/serializer": "^5.2",
"symfony/http-kernel": "^4.0|^5.0",
"symfony/validator": "^4.0|^5.0",
"symfony/security-guard": "^4.0|^5.0"
},
"autoload": {
"psr-4": {
"RetailCrm\\ServiceBundle\\": ""
},
"exclude-from-classmap": [
"Tests/"
]
},
"autoload-dev": {
"psr-4": {
"RetailCrm\\ServiceBundle\\Tests\\": "Tests/"
}
},
"require-dev": {
"ext-json": "*",
"phpunit/phpunit": "^8.0 || ^9.0",
"doctrine/annotations": "^1.11",
"doctrine/cache": "^1.10"
},
"scripts": {
"tests": "./vendor/bin/phpunit -c phpunit.xml.dist"
}
}

33
phpunit.xml.dist Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
bootstrap="vendor/autoload.php"
>
<coverage>
<include>
<directory>./</directory>
</include>
<exclude>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
<report>
<clover outputFile="coverage.xml"/>
</report>
</coverage>
<testsuites>
<testsuite name="RetailCRM Service Bundle Tests">
<directory>./Tests</directory>
</testsuite>
</testsuites>
<logging>
<junit outputFile="test-report.xml"/>
</logging>
</phpunit>