diff --git a/README.md b/README.md index 7688e72..fd26416 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,12 @@ Mock server options can be set via environment variables. * _Default value_: `disabled` * _Possible values_: `disabled`, `url_md5`, `url_and_timestamp_md5` +#### SWAGGER_MOCK_CORS_ENABLE + + * When enabled, CORS request will automatically be handled + * _Default value_: `False` + * _Possible values_: `True` or `False` + ### Specification cache To speed up server response time you can use caching mechanism for OpenAPI. There are several caching strategies. Specific strategy can be set by environment variable `SWAGGER_MOCK_CACHE_STRATEGY`. diff --git a/config/services.yaml b/config/services.yaml index 473b3d0..1401ae3 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -3,6 +3,7 @@ parameters: env(SWAGGER_MOCK_CACHE_STRATEGY): 'disabled' env(SWAGGER_MOCK_CACHE_DIRECTORY): '/dev/shm/openapi-cache' env(SWAGGER_MOCK_CACHE_TTL): 0 + env(SWAGGER_MOCK_CORS_ENABLE): False locale: 'en' specification_url: '%env(SWAGGER_MOCK_SPECIFICATION_URL)%' @@ -10,6 +11,7 @@ parameters: cache_directory: '%env(SWAGGER_MOCK_CACHE_DIRECTORY)%' cache_ttl: '%env(SWAGGER_MOCK_CACHE_TTL)%' log_level: '%env(SWAGGER_MOCK_LOG_LEVEL)%' + cors_enable: '%env(SWAGGER_MOCK_CORS_ENABLE)%' type_parser_map: object: 'App\OpenAPI\Parsing\Type\Composite\ObjectTypeParser' @@ -63,6 +65,12 @@ services: tags: - { name: kernel.event_listener, event: kernel.request, priority: 48 } + App\EventListener\CorsResponseListener: + arguments: + - '%cors_enable%' + tags: + - { name: kernel.event_listener, event: kernel.response, priority: 46 } + App\Mock\EndpointRepository: arguments: - '@specification_loader' diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..e69de29 diff --git a/src/EventListener/CorsResponseListener.php b/src/EventListener/CorsResponseListener.php new file mode 100644 index 0000000..42cd2dc --- /dev/null +++ b/src/EventListener/CorsResponseListener.php @@ -0,0 +1,52 @@ +cors_enable = $cors_enable; + } + + public function onKernelResponse(ResponseEvent $event): void + { + if (!$this->cors_enable) { + return; + } + + $request = $event->getRequest(); + + if (!$request->headers->has('Origin')) { + return; + } + + $response = $event->getResponse(); + + $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin')); + + $response->headers->set( + 'Access-Control-Allow-Methods', + $request->headers->get('Access-Control-Request-Method', 'GET,POST,PUT,DELETE') + ); + + if ($requestHeaders = $request->headers->get('Access-Control-Request-Headers')) { + $response->headers->set('Access-Control-Allow-Headers', $requestHeaders); + } + + if ($request->isMethod('OPTIONS')) { + if ($response->getStatusCode() === Response::HTTP_NOT_FOUND) { + $response->setContent(''); + $response->setStatusCode(Response::HTTP_NO_CONTENT); + } + } + } +} diff --git a/tests/Unit/EventListener/CorsResponseListenerTest.php b/tests/Unit/EventListener/CorsResponseListenerTest.php new file mode 100644 index 0000000..c53ba1d --- /dev/null +++ b/tests/Unit/EventListener/CorsResponseListenerTest.php @@ -0,0 +1,93 @@ +event = \Phake::mock(ResponseEvent::class); + } + + /** @test */ + public function onUnhandledOptionsRequest_ifFeatureEnabled_handlesCors(): void + { + $listener = new CorsResponseListener(true); + $request = new Request(); + $request->setMethod('OPTIONS'); + $request->headers->set('Origin', 'http://example.tld'); + $response = new Response(); + $response->setStatusCode(Response::HTTP_NOT_FOUND); + \Phake::when($this->event) + ->getRequest() + ->thenReturn($request); + \Phake::when($this->event) + ->getResponse() + ->thenReturn($response); + + $listener->onKernelResponse($this->event); + + $this->assertEquals(Response::HTTP_NO_CONTENT, $response->getStatusCode()); + $this->assertEquals('http://example.tld', $response->headers->get('Access-Control-Allow-Origin')); + $this->assertEquals('GET,POST,PUT,DELETE', $response->headers->get('Access-Control-Allow-Methods')); + $this->assertEquals('', $response->getContent()); + } + + /** @test */ + public function onCorsRequest_ifFeatureEnabled_addsCorsHeaders(): void + { + $listener = new CorsResponseListener(true); + $request = new Request(); + $request->setMethod('GET'); + $request->headers->set('Origin', 'http://example.tld'); + $response = new Response(); + $response->setStatusCode(Response::HTTP_OK); + $response->setContent('a content'); + \Phake::when($this->event) + ->getRequest() + ->thenReturn($request); + \Phake::when($this->event) + ->getResponse() + ->thenReturn($response); + + $listener->onKernelResponse($this->event); + + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + $this->assertEquals('a content', $response->getContent()); + $this->assertEquals('http://example.tld', $response->headers->get('Access-Control-Allow-Origin')); + $this->assertEquals('GET,POST,PUT,DELETE', $response->headers->get('Access-Control-Allow-Methods')); + } + + /** @test */ + public function onCorsRequest_ifFeatureDisabled_doesNothing(): void + { + $listener = new CorsResponseListener(false); + $request = new Request(); + $request->headers->set('Origin', 'http://example.tld'); + $response = new Response(); + $response->setStatusCode(Response::HTTP_NOT_FOUND); + \Phake::when($this->event) + ->getRequest() + ->thenReturn($request); + \Phake::when($this->event) + ->getResponse() + ->thenReturn($response); + + $listener->onKernelResponse($this->event); + + $this->assertEquals(Response::HTTP_NOT_FOUND, $response->getStatusCode()); + $this->assertEquals('', $response->headers->get('Access-Control-Allow-Origin')); + $this->assertEquals('', $response->headers->get('Access-Control-Allow-Methods')); + } +}