acquia_search-3.0.1/tests/src/Unit/SearchApiSolrAcquiaConnectorTest.php
tests/src/Unit/SearchApiSolrAcquiaConnectorTest.php
<?php
declare(strict_types=1);
namespace Drupal\Tests\acquia_search\Unit;
use Drupal\acquia_connector\Settings;
use Drupal\acquia_connector\Subscription;
use Drupal\acquia_search\AcquiaCryptConnector;
use Drupal\acquia_search\AcquiaSearchApiClient;
use Drupal\acquia_search\AcquiaSearchEvents;
use Drupal\acquia_search\Client\Solarium\AcquiaGuzzle;
use Drupal\acquia_search\Client\Solarium\Endpoint as AcquiaEndpoint;
use Drupal\acquia_search\Event\AcquiaPossibleCoresEvent;
use Drupal\acquia_search\EventSubscriber\SearchSubscriber;
use Drupal\acquia_search\Helper\Flood;
use Drupal\acquia_search\Plugin\search_api\backend\AcquiaSearchSolrBackend;
use Drupal\acquia_search\Plugin\SolrConnector\SearchApiSolrAcquiaConnector;
use Drupal\acquia_search\PreferredCoreServiceFactory;
use Drupal\Component\Datetime\Time;
use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Http\ClientFactory;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Url;
use Drupal\search_api\ServerInterface;
use Drupal\search_api\Utility\DataTypeHelperInterface;
use Drupal\search_api\Utility\FieldsHelperInterface;
use Drupal\search_api_solr\SolrConnector\SolrConnectorPluginManager;
use Drupal\search_api_solr\SolrConnectorInterface;
use Drupal\Tests\UnitTestCase;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Psr\Log\LoggerInterface;
use Solarium\Core\Event\Events;
use Solarium\Core\Query\Helper;
use Solarium\QueryType\MorelikeThis\Query as MoreLikeThisQuery;
use Solarium\QueryType\Update\Query\Query as UpdateQuery;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* @group acquia_search
* @coversDefaultClass \Drupal\acquia_search\Plugin\SolrConnector\SearchApiSolrAcquiaConnector
*/
final class SearchApiSolrAcquiaConnectorTest extends UnitTestCase {
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcher|void
*/
private $eventDispatcher;
public function testCoreLink(): void {
$container = $this->createContainerMock();
$sut = $this->createInstance($container);
// '<a href="https://foobarbaz.host:443/solr/FooBarBaz/">https://foobarbaz.host:443/solr/FooBarBaz/</a>'
self::assertEquals(
[
'#type' => 'link',
'#url' => Url::fromUri('https://solr.acquia.com:443/solr/abc123.prod/'),
'#title' => 'https://solr.acquia.com:443/solr/abc123.prod/',
],
$sut->getCoreLink()->toRenderable()
);
}
public function testCoreLinkWithExistingConfiguration(): void {
$container = $this->createContainerMock();
$sut = $this->createInstance($container, [
'port' => '8983',
'scheme' => 'http',
]);
// '<a href="https://foobarbaz.host:443/solr/FooBarBaz/">https://foobarbaz.host:443/solr/FooBarBaz/</a>'
self::assertEquals(
[
'#type' => 'link',
'#url' => Url::fromUri('https://solr.acquia.com:443/solr/abc123.prod/'),
'#title' => 'https://solr.acquia.com:443/solr/abc123.prod/',
],
$sut->getCoreLink()->toRenderable()
);
}
public function testPingServer(): void {
$ping_response = Json::encode([
'core' => [
'schema' => 'drupal-4.2.6-solr-8.x-0',
],
]);
$container = $this->createContainerMock([
'/solr/abc123.prod/admin/system' => new Response(200, [], $ping_response),
]);
$sut = $this->createInstance($container);
self::assertNotFalse($sut->pingServer());
// Verify result is statically cached, no 2nd request.
self::assertNotFalse($sut->pingServer());
}
public function testAdjustTimeout(): void {
$container = $this->createContainerMock([]);
$sut = $this->createInstance($container);
$endpoint = NULL;
$sut->adjustTimeout(15, SolrConnectorInterface::QUERY_TIMEOUT, $endpoint);
self::assertInstanceOf(AcquiaEndpoint::class, $endpoint, 'Custom endpoint class preserved');
self::assertEquals(15, $endpoint->getOption(SolrConnectorInterface::QUERY_TIMEOUT));
}
public function testGetEndpoint(): void {
$container = $this->createContainerMock([]);
$sut = $this->createInstance($container);
self::assertInstanceOf(AcquiaEndpoint::class, $sut->getEndpoint());
}
public function testGetUpdateQuery(): void {
$container = $this->createContainerMock([]);
$sut = $this->createInstance($container);
self::assertInstanceOf(UpdateQuery::class, $sut->getUpdateQuery());
// This is normally modified during hook_search_api_server_load().
$options = $sut->getEndpoint()->getOptions();
$options['overridden_by_acquia_search'] = SearchApiSolrAcquiaConnector::READ_ONLY;
$sut->getEndpoint()->setOptions($options, TRUE);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('The Search API Server serving this index is currently in read-only mode.');
$sut->getUpdateQuery();
}
/**
* @testWith [null]
* ["extract/tika"]
* ["update/extract"]
*/
public function testGetExtractQuery(?string $extract_handler): void {
$container = $this->createContainerMock([], [
'extract_query_handler_option' => $extract_handler,
]);
$sut = $this->createInstance($container);
$query = $sut->getExtractQuery();
self::assertEquals($extract_handler ?: 'update/extract', $query->getHandler());
}
public function testMoreLikeThisQuery(): void {
$container = $this->createContainerMock([]);
$sut = $this->createInstance($container);
$query = $sut->getMoreLikeThisQuery();
self::assertInstanceOf(MoreLikeThisQuery::class, $query);
self::assertEquals('mlt', $query->getHandler());
// @todo the params is the only difference from parent class. can we document why?
self::assertEquals([
'qt' => 'mlt',
], $query->getParams());
}
private function createInstance(ContainerInterface $container, array $configuration = []): SearchApiSolrAcquiaConnector {
$server = $this->createMock(ServerInterface::class);
$server->method('id')->willReturn('foobar');
$configuration['#server'] = $server;
$backend = AcquiaSearchSolrBackend::create(
$container,
$configuration,
'acquia_search_solr',
[
'label' => 'Acquia Search Solr',
'description' => '',
]
);
return $backend->getSolrConnector();
}
private function createContainerMock(array $solr_responses = [], array $subscription_data = []): ContainerInterface {
$container = new ContainerBuilder();
$state = $this->createMock(StateInterface::class);
$logger_factory = $this->createMock(LoggerChannelFactoryInterface::class);
$logger_factory->method('get')->willReturn($this->createMock(LoggerInterface::class));
$date_formatter = $this->createMock(DateFormatterInterface::class);
$messenger = $this->createMock(MessengerInterface::class);
$cache_default = $this->getMockBuilder('Drupal\Core\Cache\MemoryBackend')->disableOriginalConstructor()->getMock();
// Effectively disable the memory cache.
$cache_default->method('get')->willReturn(FALSE);
$datetime_time = new Time(new RequestStack());
$subscription = $this->createMock(Subscription::class);
$settings = $this->createMock(Settings::class);
$settings->method('getIdentifier')->willReturn('abc123');
$settings->method('getApplicationUuid')->willReturn('a47ac10b-58cc-4372-a567-0e02b2c3d470');
$settings->method('getSecretKey')->willReturn('FooBar');
$subscription->method('isActive')->willReturn(TRUE);
$subscription->method('getSettings')->willReturn($settings);
$subscription->method('getSubscription')->willReturn([
'active' => TRUE,
'uuid' => '',
'subscription_name' => '',
'expiration_date' => '',
'acquia_search' => array_filter($subscription_data + [
'api_host' => 'https://api.sr-prod02.acquia.com',
'extract_query_handler_option' => 'update/extract',
'read_only' => FALSE,
'override_search_core' => NULL,
'module_version' => \Drupal::VERSION,
]),
]);
$acquia_guzzle = new AcquiaGuzzle(
new HandlerStack(
function (Request $request) use ($solr_responses) {
$uri = $request->getUri();
self::assertArrayHasKey($uri->getPath(), $solr_responses);
$response = $solr_responses[$uri->getPath()];
assert($response instanceof Response);
$hmac = [];
$cookie_to_parseable_string = implode('&', array_map(
'trim',
explode(';', $request->getHeaderLine('Cookie'))
));
parse_str($cookie_to_parseable_string, $hmac);
$derived_key = AcquiaCryptConnector::createDerivedKey(
Crypt::hashBase64('abc123.prod'),
'abc123.prod',
'ubersecret',
);
return $response->withHeader(
'Pragma',
'hmac_digest=' . hash_hmac('sha1', $hmac['acquia_solr_nonce'] . $response->getBody(), $derived_key) . ';'
);
}
)
);
$client = new Client([
'handler' => HandlerStack::create(
new MockHandler([
new Response(200, [], Json::encode([
'data' => [
[
'id' => 'foobarprodbaz',
'attributes' => [
'url' => 'https://foobar.acquia.com/core/foobarprodbaz',
],
],
[
'id' => 'prod_foobarbaz',
'attributes' => [
'url' => 'https://foobar.acquia.com/core/prod_foobarbaz',
],
],
[
'id' => 'abc123.prod',
'attributes' => [
'url' => 'https://solr.acquia.com/core/abc123.prod',
],
],
[
'id' => 'abc123.prod.othersite',
'attributes' => [
'url' => 'https://solr.acquia.com/core/abc123.prod.othersite',
],
],
[
'id' => 'abc123.prod.default',
'attributes' => [
'url' => 'https://solr.acquia.com/core/abc123.prod.default',
],
],
],
])),
new Response(200, [], Json::encode([
'key' => 'abc123.prod',
'secret_key' => 'ubersecret',
'product_policies' => [
'salt' => Crypt::hashBase64('abc123.prod'),
],
])),
])
),
]);
$client_factory = $this->createMock(ClientFactory::class);
$client_factory->method('fromOptions')->willReturn($client);
$lock = $this->createMock(LockBackendInterface::class);
$lock->method('acquire')
->with('acquia_search_get_search_indexes')
->willReturn(TRUE);
$api_client = new AcquiaSearchApiClient(
$this->createMock(LoggerChannelInterface::class),
$subscription,
$client_factory,
$cache_default,
$datetime_time,
$lock
);
$flood = $this->createMock(Flood::class);
$flood->method('isAllowed')
->willReturnMap([
['admin/system', TRUE],
]);
$search_subscriber = new SearchSubscriber($subscription, $api_client, $flood);
if (version_compare(\Drupal::VERSION, '10.0', '<')) {
$event_dispatcher = new ContainerAwareEventDispatcher($container, [
AcquiaSearchEvents::GET_POSSIBLE_CORES => [
0 => [
[
'callable' => function (AcquiaPossibleCoresEvent $event) {
$event->addPossibleCore('abc123.prod');
},
],
],
],
Events::PRE_EXECUTE_REQUEST => [
0 => [
[
'service' => [
'acquia_search.search_subscriber',
'preExecuteRequest',
],
],
],
],
Events::POST_EXECUTE_REQUEST => [
0 => [
[
'service' => [
'acquia_search.search_subscriber',
'postExecuteRequest',
],
],
],
],
]);
}
else {
$event_dispatcher = (new \ReflectionClass(EventDispatcher::class))->newInstanceWithoutConstructor();
$event_dispatcher->addListener(
AcquiaSearchEvents::GET_POSSIBLE_CORES,
function (AcquiaPossibleCoresEvent $event) {
$event->addPossibleCore('abc123.prod');
}
);
$event_dispatcher->addListener(
Events::PRE_EXECUTE_REQUEST,
[
$search_subscriber,
'preExecuteRequest',
],
);
$event_dispatcher->addListener(
Events::POST_EXECUTE_REQUEST,
[
$search_subscriber,
'postExecuteRequest',
],
);
}
$preferred_core = new PreferredCoreServiceFactory(
$event_dispatcher,
$subscription,
$api_client,
$this->createMock(ModuleHandlerInterface::class)
);
$container->set('state', $state);
$container->set('logger.factory', $logger_factory);
$container->set('acquia_search.solarium.guzzle', $acquia_guzzle);
$container->set('date.formatter', $date_formatter);
$container->set('messenger', $messenger);
$container->set('cache.default', $cache_default);
$container->set('acquia_connector.subscription', $subscription);
$container->set('acquia_search.api_client', $api_client);
$container->set('datetime.time', $datetime_time);
$container->set('acquia_search.preferred_core_factory', $preferred_core);
$container->set('event_dispatcher', $event_dispatcher);
$container->set('acquia_search.search_subscriber', $search_subscriber);
$container->set('module_handler', $this->createMock(ModuleHandlerInterface::class));
$container->set('config.factory', $this->getConfigFactoryStub([
'search_api_solr.settings' => [],
]));
$container->set('language_manager', $this->createMock(LanguageManagerInterface::class));
$plugin_manager_search_api_solr_connector = $this->createMock(SolrConnectorPluginManager::class);
$plugin_manager_search_api_solr_connector->method('createInstance')
->willReturnCallback(function (string $plugin_id, array $config) use ($container) {
return SearchApiSolrAcquiaConnector::create($container, $config, $plugin_id, [
'label' => 'Acquia Search Connector',
'description' => '',
]);
});
$container->set('plugin.manager.search_api_solr.connector', $plugin_manager_search_api_solr_connector);
$container->set('search_api.fields_helper', $this->createMock(FieldsHelperInterface::class));
$container->set('search_api.data_type_helper', $this->createMock(DataTypeHelperInterface::class));
$container->set('solarium.query_helper', new Helper());
$container->set('entity_type.manager', $this->createMock(EntityTypeManagerInterface::class));
$container->set('lock', $this->createMock(LockBackendInterface::class));
$container->set('extension.list.module', $this->createMock(ModuleExtensionList::class));
\Drupal::setContainer($container);
return $container;
}
}
