Skip to content

Commit

Permalink
Allow API responses to be mocked in tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mxr576 committed May 14, 2019
1 parent 4f97bbf commit fef3c5e
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 2 deletions.
3 changes: 2 additions & 1 deletion modules/apigee_edge_debug/src/SDKConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Apigee\Edge\ClientInterface;
use Drupal\apigee_edge\SDKConnector as OriginalSDKConnector;
use Drupal\apigee_edge\SDKConnectorInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\key\KeyInterface;
use Http\Message\Authentication;

Expand Down Expand Up @@ -92,7 +93,7 @@ public function getClient(?Authentication $authentication = NULL, ?string $endpo
return $this->defaultClient;
}

return $this->innerService->getClient($authentication, $endpoint, array_merge($options, $extra_options));
return $this->innerService->getClient($authentication, $endpoint, NestedArray::mergeDeep($options, $extra_options));
}

/**
Expand Down
12 changes: 12 additions & 0 deletions tests/modules/apigee_edge_test/apigee_edge_test.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ services:
# Without this property for decorated services, serialization will fail. @see: https://www.drupal.org/project/drupal/issues/2536370
_serviceId: apigee_edge.sdk_connector

apigee_edge_test.http_middleware.mock_api_request:
class: Drupal\apigee_edge_test\HttpClientMiddleware\MockApiRequestEventDispatcher
arguments: ['@event_dispatcher']
tags:
- { name: http_client_middleware }

apigee_edge_test.mock_api_request_subscriber.api_response_from_state:
class: Drupal\apigee_edge_test\EventSubscriber\MockApiRequestSubscriber\ApiResponseFromState
arguments: ['@state', '@settings']
tags:
- { name: event_subscriber }

apigee_edge_test.converter.user_developer:
class: Drupal\apigee_edge_test\UserDeveloperConverter
decorates: apigee_edge.converter.user_developer
Expand Down
102 changes: 102 additions & 0 deletions tests/modules/apigee_edge_test/src/Event/ApiRequestEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

/**
* Copyright 2018 Google Inc.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/

namespace Drupal\apigee_edge_test\Event;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\EventDispatcher\Event;

/**
* A mock-able API request event to the Apigee backend.
*
* An event subscriber (mock API request subscriber) can decide whether it
* reacts to an API request or not. Every API request subscriber gets called for
* an API request - unless stopPropagation() method had been called on an event
* by a subscriber which aborts the execution of further subscribers - multiple
* subscriber can perform an action for a request but only one mock API response
* gets return to an API request.
*
* @see \Drupal\apigee_edge_test\HttpClientMiddleware\MockApiRequestEventDispatcher
*/
final class ApiRequestEvent extends Event {

/**
* The event name.
*
* @var string
*/
public const EVENT_NAME = 'apigee_edge_test.mock_http_request_event';

/**
* A mock API response if any.
*
* @var \Psr\Http\Message\ResponseInterface|null
*/
protected $response;

/**
* The API request.
*
* @var \Psr\Http\Message\RequestInterface
*/
private $request;

/**
* ApiRequestEvent constructor.
*
* @param \Psr\Http\Message\RequestInterface $request
* The API request.
*/
public function __construct(RequestInterface $request) {
$this->request = $request;
}

/**
* The API request.
*
* @return \Psr\Http\Message\RequestInterface
* The API request.
*/
public function getRequest(): RequestInterface {
return $this->request;
}

/**
* The mock API response, if any.
*
* @return \Psr\Http\Message\ResponseInterface|null
* The mock API response for the request or null.
*/
public function getResponse(): ?ResponseInterface {
return $this->response;
}

/**
* The mock API response for a request.
*
* @param \Psr\Http\Message\ResponseInterface|null $response
* The mock API response or null if there is no response.
*/
public function setResponse(?ResponseInterface $response): void {
$this->response = $response;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<?php

/**
* Copyright 2018 Google Inc.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/

namespace Drupal\apigee_edge_test\EventSubscriber\MockApiRequestSubscriber;

use Drupal\apigee_edge_test\Event\ApiRequestEvent;
use Drupal\Core\Site\Settings;
use Drupal\Core\State\State;
use GuzzleHttp\Psr7;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* Returns mock API responses from the Drupal State key-value store.
*
* It uses the State as a FIFO storage.
*/
final class ApiResponseFromState implements EventSubscriberInterface {

/**
* The settings that controls whether mocked requests get captured or not.
*
* @var string
*/
public const SETTINGS_CAPTURE_REQUESTS = 'apigee_edge_test_state_response_provider_capture_requests';

/**
* The key that this provider uses as storage for mock responses.
*
* @var string
*/
private const STATE_KEY_RESPONSE_STORAGE = 'apigee_edge_test.mock_response_provider.state.response_storage';

/**
* The key that this provider uses as storage to captured mocked requests.
*
* @var string
*/
private const STATE_KEY_REQUEST_STORAGE = 'apigee_edge_test.mock_response_provider.state.request_storage';

/**
* The state backend.
*
* @var \Drupal\Core\State\StateInterface
*/
private $state;

/**
* Settings.
*
* @var \Drupal\Core\Site\Settings
*/
private $settings;

/**
* ApiResponseFromState constructor.
*
* @param \Drupal\Core\State\State $state
* The state backend.
* @param \Drupal\Core\Site\Settings $settings
* Settings.
*/
public function __construct(State $state, Settings $settings) {
$this->state = $state;
$this->settings = $settings;
}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
ApiRequestEvent::EVENT_NAME => 'handle',
];
}

/**
* Returns a mock API response from the state storage if it is not empty.
*
* @param \Drupal\apigee_edge_test\Event\ApiRequestEvent $event
* The API request event.
*/
public function handle(ApiRequestEvent $event): void {
$queued_responses = $this->getQueuedResponses();
if (!empty($queued_responses)) {
/** @var \Psr\Http\Message\ResponseInterface $response */
$response = array_shift($queued_responses);
$response = Psr7\parse_response($response);
$event->setResponse($response);
$this->state->set(static::STATE_KEY_RESPONSE_STORAGE, $queued_responses);
if ($this->settings->get(static::SETTINGS_CAPTURE_REQUESTS, FALSE)) {
$request_storage = $this->state->get(static::STATE_KEY_REQUEST_STORAGE, []);
$request_storage[] = Psr7\str($event->getRequest());
$this->state->set(static::STATE_KEY_REQUEST_STORAGE, $request_storage);
}
// Do not call other providers because either a request-independent API
// response is available in the storage or there should not be any
// response in the storage.
$event->stopPropagation();
}
}

/**
* Adds an API response to the queue.
*
* @param \Psr\Http\Message\ResponseInterface $response
* A mock API response.
*/
public function queueResponse(ResponseInterface $response): void {
// We must clear the static cache here because items can stack in there in
// tests.
$this->state->resetCache();
$queued_responses = $this->state->get(static::STATE_KEY_RESPONSE_STORAGE, []);
$queued_responses[] = Psr7\str($response);
$this->state->set(static::STATE_KEY_RESPONSE_STORAGE, $queued_responses);
}

/**
* Returns queued API responses.
*
* @return \Psr\Http\Message\ResponseInterface[]
* Mock API responses in the queue.
*/
public function getQueuedResponses(): array {
return $this->state->get(static::STATE_KEY_RESPONSE_STORAGE, []);
}

/**
* Clears queued mock API responses and captured mocked API requests.
*/
public function clear(): void {
$this->state->delete(static::STATE_KEY_RESPONSE_STORAGE);
$this->state->delete(static::STATE_KEY_REQUEST_STORAGE);
}

/**
* Returns mocked API requests that got mocked if capturing was enabled.
*
* @return \Psr\Http\Message\RequestInterface[]
* Captured API requests that got mocked.
*/
public function getMockedRequests(): array {
return array_map(static function (string $item) {
return Psr7\parse_request($item);
}, $this->state->get(static::STATE_KEY_REQUEST_STORAGE, []));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/**
* Copyright 2018 Google Inc.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/

namespace Drupal\apigee_edge_test\HttpClientMiddleware;

use Drupal\apigee_edge_test\Event\ApiRequestEvent;
use Drupal\apigee_edge_test\SDKConnector;
use GuzzleHttp\Promise\FulfilledPromise;
use Psr\Http\Message\RequestInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
* Allows to mock Apigee API requests.
*/
final class MockApiRequestEventDispatcher {

/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
private $eventDispatcher;

/**
* MockApiRequestEventDispatcher constructor.
*
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
*/
public function __construct(EventDispatcherInterface $event_dispatcher) {
$this->eventDispatcher = $event_dispatcher;
}

/**
* {@inheritdoc}
*/
public function __invoke() {
return function (callable $handler) {
return function (RequestInterface $request, array $options) use ($handler) {
if ($request->hasHeader(SDKConnector::HEADER)) {
$event = new ApiRequestEvent($request);
$this->eventDispatcher->dispatch(ApiRequestEvent::EVENT_NAME, $event);

if ($event->getResponse()) {
return new FulfilledPromise($event->getResponse());
}
}

// No mock API subscriber provided a mock response for this HTTP
// request. Let's call the real API backend.
return $handler($request, $options);
};
};
}

}
Loading

0 comments on commit fef3c5e

Please sign in to comment.