From d44da0d531247f2b651e71c0e099662ff7fde20f Mon Sep 17 00:00:00 2001 From: Akolzin Dmitry Date: Fri, 5 Feb 2021 14:47:54 +0300 Subject: [PATCH] Initial service bundle (#1) --- .github/workflows/ci.yml | 34 ++++ .gitignore | 6 + ArgumentResolver/AbstractValueResolver.php | 43 +++++ ArgumentResolver/CallbackValueResolver.php | 86 ++++++++++ ArgumentResolver/ClientValueResolver.php | 93 +++++++++++ DependencyInjection/Configuration.php | 46 ++++++ .../RetailCrmServiceExtension.php | 63 ++++++++ .../InvalidRequestArgumentException.php | 38 +++++ Models/Error.php | 26 +++ Resources/doc/index.md | 148 ++++++++++++++++++ Response/ErrorJsonResponseFactory.php | 44 ++++++ RetailCrmServiceBundle.php | 9 ++ Security/AbstractClientAuthenticator.php | 89 +++++++++++ Security/CallbackClientAuthenticator.php | 29 ++++ Security/FrontApiClientAuthenticator.php | 52 ++++++ .../CallbackValueResolverTest.php | 113 +++++++++++++ .../ClientValueResolverTest.php | 136 ++++++++++++++++ Tests/DataFixtures/RequestDto.php | 19 +++ Tests/DataFixtures/User.php | 37 +++++ .../DependencyInjection/ConfigurationTest.php | 77 +++++++++ .../RetailCrmServiceExtensionTest.php | 55 +++++++ .../Response/ErrorJsonResponseFactoryTest.php | 35 +++++ .../CallbackClientAuthenticatorTest.php | 135 ++++++++++++++++ .../FrontApiClientAuthenticatorTest.php | 123 +++++++++++++++ composer.json | 43 +++++ phpunit.xml.dist | 33 ++++ 26 files changed, 1612 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 ArgumentResolver/AbstractValueResolver.php create mode 100644 ArgumentResolver/CallbackValueResolver.php create mode 100644 ArgumentResolver/ClientValueResolver.php create mode 100644 DependencyInjection/Configuration.php create mode 100644 DependencyInjection/RetailCrmServiceExtension.php create mode 100644 Exceptions/InvalidRequestArgumentException.php create mode 100644 Models/Error.php create mode 100644 Resources/doc/index.md create mode 100644 Response/ErrorJsonResponseFactory.php create mode 100644 RetailCrmServiceBundle.php create mode 100644 Security/AbstractClientAuthenticator.php create mode 100644 Security/CallbackClientAuthenticator.php create mode 100644 Security/FrontApiClientAuthenticator.php create mode 100644 Tests/ArgumentResolver/CallbackValueResolverTest.php create mode 100644 Tests/ArgumentResolver/ClientValueResolverTest.php create mode 100644 Tests/DataFixtures/RequestDto.php create mode 100644 Tests/DataFixtures/User.php create mode 100644 Tests/DependencyInjection/ConfigurationTest.php create mode 100644 Tests/DependencyInjection/RetailCrmServiceExtensionTest.php create mode 100644 Tests/Response/ErrorJsonResponseFactoryTest.php create mode 100644 Tests/Security/CallbackClientAuthenticatorTest.php create mode 100644 Tests/Security/FrontApiClientAuthenticatorTest.php create mode 100644 composer.json create mode 100644 phpunit.xml.dist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6df4c2b --- /dev/null +++ b/.github/workflows/ci.yml @@ -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) diff --git a/.gitignore b/.gitignore index 3dab634..7690d76 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ !bin/console !bin/symfony_requirements /vendor/ +composer.lock # Assets and user uploads /web/bundles/ @@ -38,6 +39,8 @@ # PHPUnit /app/phpunit.xml /phpunit.xml +.phpunit.result.cache +test-report.xml # Build data /build/ @@ -50,3 +53,6 @@ # Embedded web-server pid file /.web-server-pid + +.idea +coverage.xml diff --git a/ArgumentResolver/AbstractValueResolver.php b/ArgumentResolver/AbstractValueResolver.php new file mode 100644 index 0000000..e840f02 --- /dev/null +++ b/ArgumentResolver/AbstractValueResolver.php @@ -0,0 +1,43 @@ +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 + ); + } + } +} diff --git a/ArgumentResolver/CallbackValueResolver.php b/ArgumentResolver/CallbackValueResolver.php new file mode 100644 index 0000000..063a4b5 --- /dev/null +++ b/ArgumentResolver/CallbackValueResolver.php @@ -0,0 +1,86 @@ +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; + } +} diff --git a/ArgumentResolver/ClientValueResolver.php b/ArgumentResolver/ClientValueResolver.php new file mode 100644 index 0000000..0e0f940 --- /dev/null +++ b/ArgumentResolver/ClientValueResolver.php @@ -0,0 +1,93 @@ +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'); + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php new file mode 100644 index 0000000..b2f4693 --- /dev/null +++ b/DependencyInjection/Configuration.php @@ -0,0 +1,46 @@ +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; + } +} diff --git a/DependencyInjection/RetailCrmServiceExtension.php b/DependencyInjection/RetailCrmServiceExtension.php new file mode 100644 index 0000000..49c1b44 --- /dev/null +++ b/DependencyInjection/RetailCrmServiceExtension.php @@ -0,0 +1,63 @@ +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); + } +} diff --git a/Exceptions/InvalidRequestArgumentException.php b/Exceptions/InvalidRequestArgumentException.php new file mode 100644 index 0000000..4d990f1 --- /dev/null +++ b/Exceptions/InvalidRequestArgumentException.php @@ -0,0 +1,38 @@ +validateErrors = $errors; + } + + /** + * @return iterable + */ + public function getValidateErrors(): iterable + { + return $this->validateErrors; + } +} diff --git a/Models/Error.php b/Models/Error.php new file mode 100644 index 0000000..ef6c462 --- /dev/null +++ b/Models/Error.php @@ -0,0 +1,26 @@ + ['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' + ); + // ... + } +} + +``` diff --git a/Response/ErrorJsonResponseFactory.php b/Response/ErrorJsonResponseFactory.php new file mode 100644 index 0000000..76bbcf5 --- /dev/null +++ b/Response/ErrorJsonResponseFactory.php @@ -0,0 +1,44 @@ +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 + ); + } +} diff --git a/RetailCrmServiceBundle.php b/RetailCrmServiceBundle.php new file mode 100644 index 0000000..ec08b5a --- /dev/null +++ b/RetailCrmServiceBundle.php @@ -0,0 +1,9 @@ +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; + } +} diff --git a/Security/CallbackClientAuthenticator.php b/Security/CallbackClientAuthenticator.php new file mode 100644 index 0000000..698e924 --- /dev/null +++ b/Security/CallbackClientAuthenticator.php @@ -0,0 +1,29 @@ +request->has(static::AUTH_FIELD) || $request->query->has(static::AUTH_FIELD); + } + + /** + * {@inheritdoc } + */ + public function supportsRememberMe(): bool + { + return false; + } +} diff --git a/Security/FrontApiClientAuthenticator.php b/Security/FrontApiClientAuthenticator.php new file mode 100644 index 0000000..af4fe26 --- /dev/null +++ b/Security/FrontApiClientAuthenticator.php @@ -0,0 +1,52 @@ +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; + } +} diff --git a/Tests/ArgumentResolver/CallbackValueResolverTest.php b/Tests/ArgumentResolver/CallbackValueResolverTest.php new file mode 100644 index 0000000..660653d --- /dev/null +++ b/Tests/ArgumentResolver/CallbackValueResolverTest.php @@ -0,0 +1,113 @@ +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(); + } +} diff --git a/Tests/ArgumentResolver/ClientValueResolverTest.php b/Tests/ArgumentResolver/ClientValueResolverTest.php new file mode 100644 index 0000000..0b27a2f --- /dev/null +++ b/Tests/ArgumentResolver/ClientValueResolverTest.php @@ -0,0 +1,136 @@ +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(); + } +} diff --git a/Tests/DataFixtures/RequestDto.php b/Tests/DataFixtures/RequestDto.php new file mode 100644 index 0000000..f6a3d84 --- /dev/null +++ b/Tests/DataFixtures/RequestDto.php @@ -0,0 +1,19 @@ + [ + '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']); + } +} diff --git a/Tests/DependencyInjection/RetailCrmServiceExtensionTest.php b/Tests/DependencyInjection/RetailCrmServiceExtensionTest.php new file mode 100644 index 0000000..471f198 --- /dev/null +++ b/Tests/DependencyInjection/RetailCrmServiceExtensionTest.php @@ -0,0 +1,55 @@ +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)); + } +} diff --git a/Tests/Response/ErrorJsonResponseFactoryTest.php b/Tests/Response/ErrorJsonResponseFactoryTest.php new file mode 100644 index 0000000..e6d892e --- /dev/null +++ b/Tests/Response/ErrorJsonResponseFactoryTest.php @@ -0,0 +1,35 @@ +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()); + } +} diff --git a/Tests/Security/CallbackClientAuthenticatorTest.php b/Tests/Security/CallbackClientAuthenticatorTest.php new file mode 100644 index 0000000..12befe7 --- /dev/null +++ b/Tests/Security/CallbackClientAuthenticatorTest.php @@ -0,0 +1,135 @@ +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); + } +} diff --git a/Tests/Security/FrontApiClientAuthenticatorTest.php b/Tests/Security/FrontApiClientAuthenticatorTest.php new file mode 100644 index 0000000..6ee8a4e --- /dev/null +++ b/Tests/Security/FrontApiClientAuthenticatorTest.php @@ -0,0 +1,123 @@ +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); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f4500bc --- /dev/null +++ b/composer.json @@ -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" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..ada38ee --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,33 @@ + + + + + + ./ + + + ./Tests + ./vendor + + + + + + + + ./Tests + + + + + +