diff --git a/src/Search/Indexer/ChangeSet.php b/src/Search/Indexer/ChangeSet.php new file mode 100644 index 00000000..9a5d3021 --- /dev/null +++ b/src/Search/Indexer/ChangeSet.php @@ -0,0 +1,32 @@ +inserts[] = [ + 'id' => $id, + 'json' => $json, + ]; + } + + public function addDelete(string $id) + { + $this->deletes[] = $id; + } + + public function getInserts() : array + { + return $this->inserts; + } + + public function getDeletes() : array + { + return $this->deletes; + } +} diff --git a/src/Search/Indexer/Indexer.php b/src/Search/Indexer/Indexer.php new file mode 100644 index 00000000..2aabf9a6 --- /dev/null +++ b/src/Search/Indexer/Indexer.php @@ -0,0 +1,131 @@ +logger = $logger; + $this->client = $client; + $this->validator = $validator; + $this->modelIndexer = $modelIndexer; + } + + public static function getDefaultModelIndexers(Serializer $serializer, MappedElasticsearchClient $client, $rdsArticles) : array + { + return [ + 'article' => new ResearchArticleIndexer($serializer, $rdsArticles), + 'blog-article' => new BlogArticleIndexer($serializer), + 'interview' => new InterviewIndexer($serializer), + 'reviewed-preprint' => new ReviewedPreprintIndexer($serializer, $client), + 'labs-post' => new LabsPostIndexer($serializer), + 'podcast-episode' => new PodcastEpisodeIndexer($serializer), + 'collection' => new CollectionIndexer($serializer), + + ]; + } + + public function getModelIndexer($type): ModelIndexer + { + if (!isset($this->modelIndexer[$type])) { + throw new InvalidArgumentException("The {$type} is not valid."); + } + + return $this->modelIndexer[$type]; + } + + public function index($entity): ChangeSet + { + if (!$entity instanceof Model || !$entity instanceof HasIdentifier) { + throw new InvalidArgumentException('The given Entity is not an '.Model::class.' or '.HasIdentifier::class); + } + $modelIndexer = $this->getModelIndexer($entity->getIdentifier()->getType()); + + $debugId = '<'.$entity->getIdentifier().'>'; + + $this->logger->debug($debugId.' preparing for indexing.'); + $changeSet = $modelIndexer->prepareChangeSet($entity); + + $inserts = $changeSet->getInserts(); + if (count($inserts) === 0) { + $this->logger->debug($debugId.' skipping indexing'); + } + + foreach ($inserts as $insert) { + $doc = $insert['json']; + $docId = $insert['id']; + $this->logger->debug($debugId.' importing into Elasticsearch.'); + $this->insert($doc, $docId); + + $this->logger->debug($debugId.' post validating.'); + try { + $this->postValidate($docId); + } catch (Throwable $e) { + $this->logger->error($debugId.' rolling back.', [ + 'exception' => $e, + 'document' => $result ?? null, + ]); + $this->client->deleteDocument($docId); + + // We failed. + throw new \Exception($debugId.' post validate failed.'); + } + + $this->logger->info($debugId.' successfully imported.'); + } + + foreach ($changeSet->getDeletes() as $deleteId) { + $this->logger->debug('<'.$deleteId.'> removing from index.'); + $this->client->deleteDocument($deleteId); + } + + return $changeSet; + } + + public function insert(string $json, string $id) + { + // Insert the document. + $this->client->indexJsonDocument($id, $json); + return [ + 'id' => $id, + ]; + } + + public function postValidate($id) + { + // Post-validation, we got a document. + $document = $this->client->getDocumentById($id); + Assertion::isInstanceOf($document, IsDocumentResponse::class); + $result = $document->unwrap(); + // That the document is valid JSON. + $this->validator->validateSearchResult($result, true); + } +} diff --git a/src/Search/Indexer/ModelIndexer.php b/src/Search/Indexer/ModelIndexer.php new file mode 100644 index 00000000..5d46f67d --- /dev/null +++ b/src/Search/Indexer/ModelIndexer.php @@ -0,0 +1,9 @@ +serializer = $serializer; + } + + protected function getSerializer(): Serializer + { + return $this->serializer; + } + + abstract protected function getSdkClass() : string; +} diff --git a/src/Search/Indexer/ModelIndexer/BlogArticleIndexer.php b/src/Search/Indexer/ModelIndexer/BlogArticleIndexer.php new file mode 100644 index 00000000..ba99ef7c --- /dev/null +++ b/src/Search/Indexer/ModelIndexer/BlogArticleIndexer.php @@ -0,0 +1,41 @@ +serialize($blogArticle)); + $blogArticleObject->type = 'blog-article'; + $blogArticleObject->body = $this->flattenBlocks($blogArticleObject->content ?? []); + unset($blogArticleObject->content); + $blogArticleObject->snippet = ['format' => 'json', 'value' => json_encode($this->snippet($blogArticle))]; + $this->addSortDate($blogArticleObject, $blogArticle->getPublishedDate()); + + + $changeSet->addInsert( + $blogArticleObject->type.'-'.$blogArticle->getId(), + json_encode($blogArticleObject) + ); + + return $changeSet; + } +} diff --git a/src/Search/Indexer/ModelIndexer/CollectionIndexer.php b/src/Search/Indexer/ModelIndexer/CollectionIndexer.php new file mode 100644 index 00000000..9bc2f297 --- /dev/null +++ b/src/Search/Indexer/ModelIndexer/CollectionIndexer.php @@ -0,0 +1,38 @@ +serialize($collection)); + $collectionObject->type = 'collection'; + $collectionObject->summary = $this->flattenBlocks($collectionObject->summary ?? []); + $collectionObject->snippet = ['format' => 'json', 'value' => json_encode($this->snippet($collection))]; + $this->addSortDate($collectionObject, $collection->getPublishedDate()); + + + $changeSet->addInsert( + $collectionObject->type.'-'.$collection->getId(), + json_encode($collectionObject), + ); + return $changeSet; + } +} diff --git a/src/Search/Workflow/Blocks.php b/src/Search/Indexer/ModelIndexer/Helper/Blocks.php similarity index 79% rename from src/Search/Workflow/Blocks.php rename to src/Search/Indexer/ModelIndexer/Helper/Blocks.php index 9ecc10c8..329043f0 100644 --- a/src/Search/Workflow/Blocks.php +++ b/src/Search/Indexer/ModelIndexer/Helper/Blocks.php @@ -1,17 +1,17 @@ id ?? null, @@ -27,7 +27,7 @@ final private function flattenBlock(stdClass $block) : string ])); } - final private function flattenItems(array $items) : string + protected function flattenItems(array $items) : string { return implode(' ', array_map(function ($item) { if (is_string($item)) { diff --git a/src/Search/Indexer/ModelIndexer/Helper/JsonSerializerHelper.php b/src/Search/Indexer/ModelIndexer/Helper/JsonSerializerHelper.php new file mode 100644 index 00000000..956b1e12 --- /dev/null +++ b/src/Search/Indexer/ModelIndexer/Helper/JsonSerializerHelper.php @@ -0,0 +1,48 @@ +getSerializer()->deserialize($json, $this->getSdkClass(), 'json'); + + //todo: the following code causes a conflict when reading from $cache + $key = sha1($json); + if (!isset(self::$cache[$key])) { + self::$cache[$key] = $this->getSerializer()->deserialize($json, $this->getSdkClass(), 'json'); + } + + return self::$cache[$key]; + } + + protected function serialize($item) : string + { + return $this->getSerializer()->serialize($item, 'json'); + + //todo: the following code causes a conflict when reading from $cache + $key = spl_object_hash($item); + if (!isset(self::$cache[$key])) { + self::$cache[$key] = $this->getSerializer()->serialize($item, 'json'); + } + + return self::$cache[$key]; + } + + protected function snippet($item) : array + { + return $this->getSerializer()->normalize( + $item, + null, + ['snippet' => true, 'type' => true] + ); + } +} diff --git a/src/Search/Workflow/SortDate.php b/src/Search/Indexer/ModelIndexer/Helper/SortDate.php similarity index 55% rename from src/Search/Workflow/SortDate.php rename to src/Search/Indexer/ModelIndexer/Helper/SortDate.php index efb30fed..b4a424e7 100644 --- a/src/Search/Workflow/SortDate.php +++ b/src/Search/Indexer/ModelIndexer/Helper/SortDate.php @@ -1,12 +1,12 @@ sortDate = $date->format('Y-m-d\TH:i:s\Z'); diff --git a/src/Search/Indexer/ModelIndexer/InterviewIndexer.php b/src/Search/Indexer/ModelIndexer/InterviewIndexer.php new file mode 100644 index 00000000..d91021d5 --- /dev/null +++ b/src/Search/Indexer/ModelIndexer/InterviewIndexer.php @@ -0,0 +1,40 @@ +serialize($interview)); + $interviewObject->type = 'interview'; + $interviewObject->body = $this->flattenBlocks($interviewObject->content ?? []); + unset($interviewObject->content); + $interviewObject->snippet = ['format' => 'json', 'value' => json_encode($this->snippet($interview))]; + // Add publish date to sort on. + $this->addSortDate($interviewObject, $interview->getPublishedDate()); + + + $changeSet->addInsert( + $interviewObject->type.'-'.$interview->getId(), + json_encode($interviewObject), + ); + return $changeSet; + } +} diff --git a/src/Search/Indexer/ModelIndexer/LabsPostIndexer.php b/src/Search/Indexer/ModelIndexer/LabsPostIndexer.php new file mode 100644 index 00000000..25302650 --- /dev/null +++ b/src/Search/Indexer/ModelIndexer/LabsPostIndexer.php @@ -0,0 +1,39 @@ +serialize($labsPost)); + $labsPostObject->type = 'labs-post'; + $labsPostObject->body = $this->flattenBlocks($labsPostObject->content ?? []); + unset($labsPostObject->content); + $labsPostObject->snippet = ['format' => 'json', 'value' => json_encode($this->snippet($labsPost))]; + $this->addSortDate($labsPostObject, $labsPost->getPublishedDate()); + + + $changeSet->addInsert( + $labsPostObject->type.'-'.$labsPost->getId(), + json_encode($labsPostObject), + ); + return $changeSet; + } +} diff --git a/src/Search/Indexer/ModelIndexer/PodcastEpisodeIndexer.php b/src/Search/Indexer/ModelIndexer/PodcastEpisodeIndexer.php new file mode 100644 index 00000000..0f4d973e --- /dev/null +++ b/src/Search/Indexer/ModelIndexer/PodcastEpisodeIndexer.php @@ -0,0 +1,38 @@ +serialize($podcastEpisode)); + $podcastEpisodeObject->type = 'podcast-episode'; + $podcastEpisodeObject->snippet = ['format' => 'json', 'value' => json_encode($this->snippet($podcastEpisode))]; + // Add sort date. + $this->addSortDate($podcastEpisodeObject, $podcastEpisode->getPublishedDate()); + + + $changeSet->addInsert( + $podcastEpisodeObject->type.'-'.$podcastEpisode->getNumber(), + json_encode($podcastEpisodeObject), + ); + return $changeSet; + } +} diff --git a/src/Search/Workflow/ResearchArticleWorkflow.php b/src/Search/Indexer/ModelIndexer/ResearchArticleIndexer.php similarity index 56% rename from src/Search/Workflow/ResearchArticleWorkflow.php rename to src/Search/Indexer/ModelIndexer/ResearchArticleIndexer.php index 86323939..73cada93 100644 --- a/src/Search/Workflow/ResearchArticleWorkflow.php +++ b/src/Search/Indexer/ModelIndexer/ResearchArticleIndexer.php @@ -1,58 +1,37 @@ serializer = $serializer; - $this->logger = $logger; - $this->client = $client; - $this->validator = $validator; + public function __construct(Serializer $serializer, array $rdsArticles = []) + { + parent::__construct($serializer); $this->rdsArticles = $rdsArticles; } + protected function getSdkClass(): string + { + return ArticleVersion::class; + } + /** - * @param ArticleVersion $article - * @return array + * @param ResearchArticle $article + * @return ChangeSet */ - public function index(Model $article) : array + public function prepareChangeSet(Model $article) : ChangeSet { - $this->logger->debug('ResearchArticle<'.$article->getId().'> Indexing '.$article->getTitle()); + $changeSet = new ChangeSet(); $articleObject = json_decode($this->serialize($article)); // Fix author name. @@ -111,8 +90,7 @@ public function index(Model $article) : array $snippet = $this->snippet($article); if ($article instanceof ArticleVoR) { - $this->logger->debug('Article<'.$article->getId().'> delete corresponding reviewed preprint from index, if exists'); - $this->client->deleteDocument('reviewed-preprint-'.$article->getId()); + $changeSet->addDelete('reviewed-preprint-'.$article->getId()); } $articleObject->snippet = ['format' => 'json', 'value' => json_encode($snippet)]; @@ -127,51 +105,10 @@ public function index(Model $article) : array } $this->addSortDate($articleObject, $sortDate); - $this->logger->debug('Article<'.$article->getId().'> Detected type '.($article->getType() ?? 'research-article')); - - return [ - 'json' => json_encode($articleObject), - 'id' => ($article->getType() ?? 'research-article').'-'.$article->getId(), - ]; - } - - public function insert(string $json, string $id, bool $skipInsert = false) - { - // Insert the document. - $this->logger->debug('ResearchArticle<'.$id.'> importing into Elasticsearch.'); - $this->client->indexJsonDocument($id, $json); - - return [ - 'id' => $id, - ]; - } - - public function postValidate(string $id, bool $skipValidate = false) : int - { - $this->logger->debug('ResearchArticle<'.$id.'> post validation.'); - try { - // Post-validation, we got a document. - $document = $this->client->getDocumentById($id); - Assertion::isInstanceOf($document, IsDocumentResponse::class); - $result = $document->unwrap(); - // That research article is valid JSON. - $this->validator->validateSearchResult($result, true); - } catch (Throwable $e) { - $this->logger->error('ResearchArticle<'.$id.'> rolling back', [ - 'exception' => $e, - ]); - $this->client->deleteDocument($id); - - // We failed. - return self::WORKFLOW_FAILURE; - } - $this->logger->info('ResearchArticle<'.$id.'> successfully imported.'); - - return self::WORKFLOW_SUCCESS; - } - - public function getSdkClass() : string - { - return ArticleVersion::class; + $changeSet->addInsert( + $articleObject->type.'-'.$article->getId(), + json_encode($articleObject), + ); + return $changeSet; } } diff --git a/src/Search/Indexer/ModelIndexer/ReviewedPreprintIndexer.php b/src/Search/Indexer/ModelIndexer/ReviewedPreprintIndexer.php new file mode 100644 index 00000000..acf0154d --- /dev/null +++ b/src/Search/Indexer/ModelIndexer/ReviewedPreprintIndexer.php @@ -0,0 +1,60 @@ +client = $client; + } + + protected function getSdkClass(): string + { + return ReviewedPreprint::class; + } + + /** + * @param ReviewedPreprint $reviewedPreprint + * @return ChangeSet + */ + public function prepareChangeSet(Model $reviewedPreprint) : ChangeSet + { + $changeSet = new ChangeSet(); + + // Don't index if article with same id present in index. + foreach ([ + 'research-article', + 'tools-resources', + 'short-report', + 'research-advance', + ] as $type) { + if ($this->client->getDocumentById($type.'-'. $reviewedPreprint->getId(), null, true) !== null) { + return $changeSet; + } + } + + $reviewedPreprintObject = json_decode($this->serialize($reviewedPreprint)); + $reviewedPreprintObject->type = 'reviewed-preprint'; + $reviewedPreprintObject->body = $reviewedPreprint->getIndexContent() ?? ''; + $reviewedPreprintObject->snippet = ['format' => 'json', 'value' => json_encode($this->snippet($reviewedPreprint))]; + + $this->addSortDate($reviewedPreprintObject, $reviewedPreprint->getStatusDate()); + + + $changeSet->addInsert( + $reviewedPreprintObject->type.'-'.$reviewedPreprint->getId(), + json_encode($reviewedPreprintObject), + ); + return $changeSet; + } +} diff --git a/src/Search/Kernel.php b/src/Search/Kernel.php index 0d6b9131..634270f9 100644 --- a/src/Search/Kernel.php +++ b/src/Search/Kernel.php @@ -39,7 +39,7 @@ use eLife\Search\Queue\Command\ImportCommand; use eLife\Search\Queue\Command\QueueWatchCommand; use eLife\Search\KeyValueStore\ElasticsearchKeyValueStore; -use eLife\Search\Queue\Workflow; +use eLife\Search\Indexer\Indexer; use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; @@ -408,13 +408,16 @@ public function dependencies(Application $app) ); }; - $app['workflow'] = function (Application $app) { - return new Workflow( - $app['api.sdk']->getSerializer(), + $app['indexer'] = function (Application $app) { + return new Indexer( $app['logger'], $app['elastic.client.write'], $app['validator'], - $app['config']['rds_articles'] + Indexer::getDefaultModelIndexers( + $app['api.sdk']->getSerializer(), + $app['elastic.client.write'], + $app['config']['rds_articles'] + ) ); }; @@ -424,7 +427,7 @@ public function dependencies(Application $app) return new QueueWatchCommand( $app['mocks.queue'], $app['mocks.queue_transformer'], - $app['workflow'], + $app['indexer'], true, $app['logger'], $app['monitoring'], @@ -435,7 +438,7 @@ public function dependencies(Application $app) return new QueueWatchCommand( $app['aws.queue'], $app['aws.queue_transformer'], - $app['workflow'], + $app['indexer'], false, $app['logger'], $app['monitoring'], diff --git a/src/Search/Queue/Command/QueueWatchCommand.php b/src/Search/Queue/Command/QueueWatchCommand.php index cdccff67..663dffee 100644 --- a/src/Search/Queue/Command/QueueWatchCommand.php +++ b/src/Search/Queue/Command/QueueWatchCommand.php @@ -7,20 +7,19 @@ use eLife\Bus\Queue\QueueItemTransformer; use eLife\Bus\Queue\WatchableQueue; use eLife\Logging\Monitoring; -use eLife\Search\Queue\Workflow; +use eLife\Search\Indexer\Indexer; use Psr\Log\LoggerInterface; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; class QueueWatchCommand extends QueueCommand { private $isMock; - private $workflow; + private $indexer; public function __construct( WatchableQueue $queue, QueueItemTransformer $transformer, - Workflow $workflow, + Indexer $indexer, bool $isMock, LoggerInterface $logger, Monitoring $monitoring, @@ -28,7 +27,7 @@ public function __construct( ) { parent::__construct($logger, $queue, $transformer, $monitoring, $limit, false); $this->isMock = $isMock; - $this->workflow = $workflow; + $this->indexer = $indexer; } protected function configure() @@ -41,9 +40,6 @@ protected function configure() protected function process(InputInterface $input, QueueItem $item, $entity = null) { - $entity = $this->transform($item); - if ($entity) { - $this->workflow->getWorkflow($item)->run($entity); - } + $this->indexer->index($entity); } } diff --git a/src/Search/Queue/Workflow.php b/src/Search/Queue/Workflow.php deleted file mode 100644 index 92adc2b4..00000000 --- a/src/Search/Queue/Workflow.php +++ /dev/null @@ -1,82 +0,0 @@ - ResearchArticleWorkflow::class, - 'blog-article' => BlogArticleWorkflow::class, - 'interview' => InterviewWorkflow::class, - 'reviewed-preprint' => ReviewedPreprintWorkflow::class, - 'labs-post' => LabsPostWorkflow::class, - 'podcast-episode' => PodcastEpisodeWorkflow::class, - 'collection' => CollectionWorkflow::class, - ]; - - public function __construct( - Serializer $serializer, - LoggerInterface $logger, - MappedElasticsearchClient $client, - HasSearchResultValidator $validator, - array $rdsArticles = [] - ) { - $this->serializer = $serializer; - $this->logger = $logger; - $this->client = $client; - $this->validator = $validator; - $this->rdsArticles = $rdsArticles; - } - - public function getWorkflow(QueueItem $item): AbstractWorkflow - { - $type = $item->getType(); - - if (isset($this->workflowClasses[$type])) { - return new $this->workflowClasses[$type]( - $this->serializer, - $this->logger, - $this->client, - $this->validator, - ...$this->getExtraArguments($type) - ); - } - - throw new \InvalidArgumentException("The {$type} is not valid."); - } - - public function process(QueueItem $item, $entity) - { - $workflow = $this->getWorkflow($item); - $workflow->run($entity); - } - - private function getExtraArguments(string $type): array - { - if ($type === 'article') { - return [$this->rdsArticles]; - } - - return []; - } -} diff --git a/src/Search/Workflow/AbstractWorkflow.php b/src/Search/Workflow/AbstractWorkflow.php deleted file mode 100644 index c0b88b59..00000000 --- a/src/Search/Workflow/AbstractWorkflow.php +++ /dev/null @@ -1,30 +0,0 @@ -index($entity); - $skipInsert = $result['skipInsert'] ?? false; - $result = $this->insert($result['json'], $result['id'], $skipInsert); - $skipValidate = $result['skipValidate'] ?? false; - if (-1 === $this->postValidate($result['id'], $skipValidate)) { - throw new Exception('post validate failed. Retrying...'); - } - } -} diff --git a/src/Search/Workflow/BlogArticleWorkflow.php b/src/Search/Workflow/BlogArticleWorkflow.php deleted file mode 100644 index efd6ef16..00000000 --- a/src/Search/Workflow/BlogArticleWorkflow.php +++ /dev/null @@ -1,105 +0,0 @@ -serializer = $serializer; - $this->logger = $logger; - $this->client = $client; - $this->validator = $validator; - } - - /** - * @param BlogArticle $blogArticle - * @return array - */ - public function index(Model $blogArticle) : array - { - $this->logger->debug('BlogArticle<'.$blogArticle->getId().'> Indexing '.$blogArticle->getTitle()); - // Normalized fields. - $blogArticleObject = json_decode($this->serialize($blogArticle)); - $blogArticleObject->type = 'blog-article'; - $blogArticleObject->body = $this->flattenBlocks($blogArticleObject->content ?? []); - unset($blogArticleObject->content); - $blogArticleObject->snippet = ['format' => 'json', 'value' => json_encode($this->snippet($blogArticle))]; - $this->addSortDate($blogArticleObject, $blogArticle->getPublishedDate()); - - // Return. - return [ - 'json' => json_encode($blogArticleObject), - 'id' => $blogArticleObject->type.'-'.$blogArticle->getId(), - ]; - } - - public function insert(string $json, string $id, bool $skipInsert = false) : array - { - // Insert the document. - $this->logger->debug('BlogArticle<'.$id.'> importing into Elasticsearch.'); - $this->client->indexJsonDocument($id, $json); - - return [ - 'id' => $id, - ]; - } - - public function postValidate(string $id, bool $skipValidate = false) : int - { - try { - // Post-validation, we got a document. - $document = $this->client->getDocumentById($id); - Assertion::isInstanceOf($document, IsDocumentResponse::class); - $result = $document->unwrap(); - // That blog article is valid JSON. - $this->validator->validateSearchResult($result, true); - } catch (Throwable $e) { - $this->logger->error('BlogArticle<'.$id.'> rolling back', [ - 'exception' => $e, - ]); - $this->client->deleteDocument($id); - - // We failed. - return self::WORKFLOW_FAILURE; - } - - $this->logger->info('BlogArticle<'.$id.'> successfully imported.'); - - return self::WORKFLOW_SUCCESS; - } - - public function getSdkClass() : string - { - return BlogArticle::class; - } -} diff --git a/src/Search/Workflow/CollectionWorkflow.php b/src/Search/Workflow/CollectionWorkflow.php deleted file mode 100644 index 5810260c..00000000 --- a/src/Search/Workflow/CollectionWorkflow.php +++ /dev/null @@ -1,104 +0,0 @@ -serializer = $serializer; - $this->logger = $logger; - $this->client = $client; - $this->validator = $validator; - } - - /** - * @param Collection $collection - * @return array - */ - public function index(Model $collection) : array - { - $this->logger->debug('Collection<'.$collection->getId().'> Indexing '.$collection->getTitle()); - // Normalized fields. - $collectionObject = json_decode($this->serialize($collection)); - $collectionObject->type = 'collection'; - $collectionObject->summary = $this->flattenBlocks($collectionObject->summary ?? []); - $collectionObject->snippet = ['format' => 'json', 'value' => json_encode($this->snippet($collection))]; - $this->addSortDate($collectionObject, $collection->getPublishedDate()); - - // Return. - return [ - 'json' => json_encode($collectionObject), - 'id' => $collectionObject->type.'-'.$collection->getId(), - ]; - } - - public function insert(string $json, string $id, bool $skipInsert = false) : array - { - // Insert the document. - $this->logger->debug('Collection<'.$id.'> importing into Elasticsearch.'); - $this->client->indexJsonDocument($id, $json); - - return [ - 'id' => $id, - ]; - } - - public function postValidate(string $id, bool $skipValidate = false) : int - { - try { - // Post-validation, we got a document. - $document = $this->client->getDocumentById($id); - Assertion::isInstanceOf($document, IsDocumentResponse::class); - $result = $document->unwrap(); - // That collection is valid JSON. - $this->validator->validateSearchResult($result, true); - } catch (Throwable $e) { - $this->logger->error('Collection<'.$id.'> rolling back', [ - 'exception' => $e, - ]); - $this->client->deleteDocument($id); - - // We failed. - return self::WORKFLOW_FAILURE; - } - - $this->logger->info('Collection<'.$id.'> successfully imported.'); - - return self::WORKFLOW_SUCCESS; - } - - public function getSdkClass() : string - { - return Collection::class; - } -} diff --git a/src/Search/Workflow/InterviewWorkflow.php b/src/Search/Workflow/InterviewWorkflow.php deleted file mode 100644 index faf6b38e..00000000 --- a/src/Search/Workflow/InterviewWorkflow.php +++ /dev/null @@ -1,116 +0,0 @@ -serializer = $serializer; - $this->logger = $logger; - $this->client = $client; - $this->validator = $validator; - } - - /** - * @param Interview $interview - * @return array - */ - public function index(Model $interview) : array - { - $this->logger->debug('Interview<'.$interview->getId().'> Indexing '.$interview->getTitle()); - - // Normalized fields. - $interviewObject = json_decode($this->serialize($interview)); - $interviewObject->type = 'interview'; - $interviewObject->body = $this->flattenBlocks($interviewObject->content ?? []); - unset($interviewObject->content); - $interviewObject->snippet = ['format' => 'json', 'value' => json_encode($this->snippet($interview))]; - // Add publish date to sort on. - $this->addSortDate($interviewObject, $interview->getPublishedDate()); - - return [ - 'json' => json_encode($interviewObject), - 'id' => $interviewObject->type.'-'.$interview->getId(), - ]; - } - - public function insert(string $json, string $id, bool $skipInsert = false) - { - // Insert the document. - $this->logger->debug('Interview<'.$id.'> importing into Elasticsearch.'); - $this->client->indexJsonDocument($id, $json); - - return [ - 'id' => $id, - ]; - } - - public function postValidate(string $id, bool $skipValidate = false) : int - { - try { - // Post-validation, we got a document. - $document = $this->client->getDocumentById($id); - Assertion::isInstanceOf($document, IsDocumentResponse::class); - $result = $document->unwrap(); - // That interview is valid JSON. - $this->validator->validateSearchResult($result, true); - } catch (Throwable $e) { - $this->logger->error('Interview<'.$id.'> rolling back', [ - 'exception' => $e, - ]); - $this->client->deleteDocument($id); - - // We failed. - return self::WORKFLOW_FAILURE; - } - - $this->logger->info('Interview<'.$id.'> successfully imported.'); - - return self::WORKFLOW_SUCCESS; - } - - public function deserialize(string $json) : Interview - { - return $this->serializer->deserialize($json, Interview::class, 'json'); - } - - public function serialize(Interview $interview) : string - { - return $this->serializer->serialize($interview, 'json'); - } - - public function getSdkClass() : string - { - return Interview::class; - } -} diff --git a/src/Search/Workflow/JsonSerializeTransport.php b/src/Search/Workflow/JsonSerializeTransport.php deleted file mode 100644 index 0848f978..00000000 --- a/src/Search/Workflow/JsonSerializeTransport.php +++ /dev/null @@ -1,65 +0,0 @@ -serializer) || - null === $this->serializer || - !$this->serializer instanceof Serializer - ) { - throw new LogicException('You must inject API SDK serializer for this to work (property: $serializer missing.)'); - } - } - - public function deserialize(string $json) - { - $this->checkSerializer(); - - return $this->serializer->deserialize($json, $this->getSdkClass(), 'json'); - - //todo: the following code causes a conflict when reading from $cache - $key = sha1($json); - if (!isset(self::$cache[$key])) { - self::$cache[$key] = $this->serializer->deserialize($json, $this->getSdkClass(), 'json'); - } - - return self::$cache[$key]; - } - - public function serialize($item) : string - { - $this->checkSerializer(); - - return $this->serializer->serialize($item, 'json'); - - //todo: the following code causes a conflict when reading from $cache - $key = spl_object_hash($item); - if (!isset(self::$cache[$key])) { - self::$cache[$key] = $this->serializer->serialize($item, 'json'); - } - - return self::$cache[$key]; - } - - public function snippet($item) : array - { - $this->checkSerializer(); - - return $this->serializer->normalize( - $item, - null, - ['snippet' => true, 'type' => true] - ); - } -} diff --git a/src/Search/Workflow/LabsPostWorkflow.php b/src/Search/Workflow/LabsPostWorkflow.php deleted file mode 100644 index 11f0592f..00000000 --- a/src/Search/Workflow/LabsPostWorkflow.php +++ /dev/null @@ -1,105 +0,0 @@ -serializer = $serializer; - $this->client = $client; - $this->logger = $logger; - $this->validator = $validator; - } - - /** - * @param LabsPost $labsPost - * @return array - */ - public function index(Model $labsPost) : array - { - $this->logger->debug('LabsPost<'.$labsPost->getId().'> Indexing '.$labsPost->getTitle()); - - // Normalized fields. - $labsPostObject = json_decode($this->serialize($labsPost)); - $labsPostObject->type = 'labs-post'; - $labsPostObject->body = $this->flattenBlocks($labsPostObject->content ?? []); - unset($labsPostObject->content); - $labsPostObject->snippet = ['format' => 'json', 'value' => json_encode($this->snippet($labsPost))]; - $this->addSortDate($labsPostObject, $labsPost->getPublishedDate()); - - return [ - 'json' => json_encode($labsPostObject), - 'id' => $labsPostObject->type.'-'.$labsPost->getId(), - ]; - } - - public function insert(string $json, string $id, bool $skipInsert = false) - { - // Insert the document. - $this->logger->debug('LabsPost<'.$id.'> importing into Elasticsearch.'); - $this->client->indexJsonDocument($id, $json); - - return [ - 'id' => $id, - ]; - } - - public function postValidate(string $id, bool $skipValidate = false) : int - { - try { - // Post-validation, we got a document. - $document = $this->client->getDocumentById($id); - Assertion::isInstanceOf($document, IsDocumentResponse::class); - $result = $document->unwrap(); - // That labs post is valid JSON. - $this->validator->validateSearchResult($result, true); - } catch (Throwable $e) { - $this->logger->error('LabsPost<'.$id.'> rolling back', [ - 'exception' => $e, - ]); - $this->client->deleteDocument($id); - - // We failed. - return self::WORKFLOW_FAILURE; - } - - $this->logger->info('LabsPost<'.$id.'> successfully imported.'); - - return self::WORKFLOW_SUCCESS; - } - - public function getSdkClass() : string - { - return LabsPost::class; - } -} diff --git a/src/Search/Workflow/PodcastEpisodeWorkflow.php b/src/Search/Workflow/PodcastEpisodeWorkflow.php deleted file mode 100644 index 65f4530e..00000000 --- a/src/Search/Workflow/PodcastEpisodeWorkflow.php +++ /dev/null @@ -1,104 +0,0 @@ -serializer = $serializer; - $this->logger = $logger; - $this->client = $client; - $this->validator = $validator; - } - - /** - * @param PodcastEpisode $podcastEpisode - * @return array - */ - public function index(Model $podcastEpisode) : array - { - $this->logger->debug('indexing '.$podcastEpisode->getTitle()); - - // Normalized fields. - $podcastEpisodeObject = json_decode($this->serialize($podcastEpisode)); - $podcastEpisodeObject->type = 'podcast-episode'; - $podcastEpisodeObject->snippet = ['format' => 'json', 'value' => json_encode($this->snippet($podcastEpisode))]; - // Add sort date. - $this->addSortDate($podcastEpisodeObject, $podcastEpisode->getPublishedDate()); - - return [ - 'json' => json_encode($podcastEpisodeObject), - 'id' => $podcastEpisodeObject->type.'-'.$podcastEpisode->getNumber(), - ]; - } - - public function insert(string $json, string $id, bool $skipInsert = false) - { - // Insert the document. - $this->logger->debug('PodcastEpisode<'.$id.'> importing into Elasticsearch.'); - $this->client->indexJsonDocument($id, $json); - - return [ - 'id' => $id, - ]; - } - - public function postValidate($id, bool $skipValidate = false) : int - { - try { - // Post-validation, we got a document. - $document = $this->client->getDocumentById($id); - Assertion::isInstanceOf($document, IsDocumentResponse::class); - $result = $document->unwrap(); - // That podcast episode is valid JSON. - $this->validator->validateSearchResult($result, true); - } catch (Throwable $e) { - $this->logger->error('PodcastEpisode<'.$id.'> rolling back', [ - 'exception' => $e, - 'document' => $result ?? null, - ]); - $this->client->deleteDocument($id); - - // We failed. - return self::WORKFLOW_FAILURE; - } - - $this->logger->info('PodcastEpisode<'.$id.'> successfully imported.'); - - return self::WORKFLOW_SUCCESS; - } - - public function getSdkClass() : string - { - return PodcastEpisode::class; - } -} diff --git a/src/Search/Workflow/ReviewedPreprintWorkflow.php b/src/Search/Workflow/ReviewedPreprintWorkflow.php deleted file mode 100644 index 59301a89..00000000 --- a/src/Search/Workflow/ReviewedPreprintWorkflow.php +++ /dev/null @@ -1,129 +0,0 @@ -serializer = $serializer; - $this->logger = $logger; - $this->client = $client; - $this->validator = $validator; - } - - /** - * @param ReviewedPreprint $reviewedPreprint - */ - public function index(Model $reviewedPreprint) : array - { - // Don't index if article with same id present in index. - foreach ([ - 'research-article', - 'tools-resources', - 'short-report', - 'research-advance', - ] as $type) { - if ($this->client->getDocumentById($type.'-'. $reviewedPreprint->getId(), null, true) !== null) { - return ['json' => '', 'id' => $reviewedPreprint->getId(), 'skipInsert' => true]; - } - } - - $this->logger->debug('ReviewedPreprint<'.$reviewedPreprint->getId().'> Indexing '.$reviewedPreprint->getTitle()); - - $reviewedPreprintObject = json_decode($this->serialize($reviewedPreprint)); - $reviewedPreprintObject->type = 'reviewed-preprint'; - $reviewedPreprintObject->body = $reviewedPreprint->getIndexContent() ?? ''; - - $reviewedPreprintObject->snippet = ['format' => 'json', 'value' => json_encode($this->snippet($reviewedPreprint))]; - - $this->addSortDate($reviewedPreprintObject, $reviewedPreprint->getStatusDate()); - - $this->logger->debug('ReviewedPreprint<'.$reviewedPreprint->getId()); - - return [ - 'json' => json_encode($reviewedPreprintObject), - 'id' => $reviewedPreprintObject->type.'-'.$reviewedPreprint->getId(), - 'skipInsert' => false, - ]; - } - - public function insert(string $json, string $id, bool $skipInsert) - { - if ($skipInsert) { - $this->logger->debug('ReviewedPreprint<'.$id.'> no need to index.'); - return ['id' => $id, 'skipValidate' => true]; - } - // Insert the document. - $this->logger->debug('ReviewedPreprint<'.$id.'> importing into Elasticsearch.'); - $this->client->indexJsonDocument($id, $json); - - return [ - 'id' => $id, - 'skipValidate' => false, - ]; - } - - public function postValidate(string $id, bool $skipValidate) : int - { - if ($skipValidate) { - $this->logger->debug('ReviewedPreprint<'.$id.'> no need to validate.'); - return self::WORKFLOW_SUCCESS; - } - - $this->logger->debug('ReviewedPreprint<'.$id.'> post validation.'); - try { - // Post-validation, we got a document. - $document = $this->client->getDocumentById($id); - Assertion::isInstanceOf($document, IsDocumentResponse::class); - $result = $document->unwrap(); - // That research reviewed preprint is valid JSON. - $this->validator->validateSearchResult($result, true); - } catch (Throwable $e) { - $this->logger->error('ReviewedPreprint<'.$id.'> rolling back', [ - 'exception' => $e, - ]); - $this->client->deleteDocument($id); - - // We failed. - return self::WORKFLOW_FAILURE; - } - $this->logger->info('ReviewedPreprint<'.$id.'> successfully imported.'); - - return self::WORKFLOW_SUCCESS; - } - - public function getSdkClass() : string - { - return ReviewedPreprint::class; - } -} diff --git a/tests/src/Search/Indexer/IndexerTest.php b/tests/src/Search/Indexer/IndexerTest.php new file mode 100644 index 00000000..7497429d --- /dev/null +++ b/tests/src/Search/Indexer/IndexerTest.php @@ -0,0 +1,165 @@ +elastic = Mockery::mock(MappedElasticsearchClient::class); + $this->validator = Mockery::mock(HasSearchResultValidator::class); + $this->mockArticleIndexer = Mockery::mock(ModelIndexer::class); + + $logger = new ExceptionNullLogger(); + + $this->indexer = new Indexer($logger, $this->elastic, $this->validator, ['article' => $this->mockArticleIndexer]); + } + + public function tearDown() + { + Mockery::close(); + parent::tearDown(); + } + + private function getMockEntity($type = 'article', $id = '1') + { + return new class($type, $id) implements Model, HasIdentifier { + private $type; + private $id; + + public function __construct($type, $id) + { + $this->type = $type; + $this->id = $id; + } + public function getIdentifier(): Identifier + { + return Identifier::fromString("{$this->type}/{$this->id}"); + } + }; + } + + /** + * @test + */ + public function testSkipInsert() + { + $entity = $this->getMockEntity(); + $this->mockArticleIndexer->shouldReceive('prepareChangeSet') + ->once() + ->with($entity) + ->andReturn(new ChangeSet()); + $this->elastic->shouldNotReceive('indexJsonDocument'); + $this->elastic->shouldNotReceive('deleteDocument'); + $this->indexer->index($entity); + } + + /** + * @test + */ + public function testIndexSuccess() + { + $entity = $this->getMockEntity(); + $changeSet = new ChangeSet(); + $changeSet->addInsert('article/1', '{}'); + $this->mockArticleIndexer->shouldReceive('prepareChangeSet') + ->once() + ->with($entity) + ->andReturn($changeSet); + $this->elastic->shouldReceive('indexJsonDocument'); + $document = Mockery::mock(IsDocumentResponse::class); + $this->elastic->shouldReceive('getDocumentById') + ->once() + ->with($entity->getIdentifier()->__toString()) + ->andReturn($document); + $document->shouldReceive('unwrap') + ->once() + ->andReturn([]); + $this->validator->shouldReceive('validateSearchResult') + ->once() + ->andReturn(true); + $this->indexer->index($entity); + } + + + /** + * @test + */ + public function testPostValidateFailure() + { + $entity = $this->getMockEntity(); + $changeSet = new ChangeSet(); + $changeSet->addInsert('article/1', '{}'); + $this->mockArticleIndexer->shouldReceive('prepareChangeSet') + ->once() + ->with($entity) + ->andReturn($changeSet); + $this->elastic->shouldReceive('indexJsonDocument'); + $document = Mockery::mock(IsDocumentResponse::class); + $this->elastic->shouldReceive('getDocumentById') + ->once() + ->with($entity->getIdentifier()->__toString()) + ->andReturn($document); + $document->shouldReceive('unwrap') + ->once() + ->andReturn([]); + $this->validator->shouldReceive('validateSearchResult') + ->once() + ->andThrow(Exception::class); + $this->elastic->shouldReceive('deleteDocument') + ->once() + ->with($entity->getIdentifier()->__toString()); + $this->expectException(\Exception::class); + $this->indexer->index($entity); + } + + /** + * @test + */ + public function testIndexAndDeleteSuccess() + { + $entity = $this->getMockEntity(); + $changeSet = new ChangeSet(); + $changeSet->addInsert('article/1', '{}'); + $changeSet->addDelete('reviewed-preprint/1', '{}'); + $this->mockArticleIndexer->shouldReceive('prepareChangeSet') + ->once() + ->with($entity) + ->andReturn($changeSet); + $this->elastic->shouldReceive('indexJsonDocument'); + $document = Mockery::mock(IsDocumentResponse::class); + $this->elastic->shouldReceive('getDocumentById') + ->once() + ->with($entity->getIdentifier()->__toString()) + ->andReturn($document); + $document->shouldReceive('unwrap') + ->once() + ->andReturn([]); + $this->validator->shouldReceive('validateSearchResult') + ->once() + ->andReturn(true); + $this->elastic->shouldReceive('deleteDocument') + ->once() + ->with('reviewed-preprint/1'); + $this->indexer->index($entity); + } +} diff --git a/tests/src/Search/Indexer/ModelIndexer/BlogArticleIndexerTest.php b/tests/src/Search/Indexer/ModelIndexer/BlogArticleIndexerTest.php new file mode 100644 index 00000000..74066bee --- /dev/null +++ b/tests/src/Search/Indexer/ModelIndexer/BlogArticleIndexerTest.php @@ -0,0 +1,72 @@ +indexer = new BlogArticleIndexer($this->getSerializer()); + } + + protected function getModelDefinitions() + { + return [ + ['model' => 'blog-article', 'modelClass' => BlogArticle::class, 'version' => 2] + ]; + } + + + /** + * @dataProvider modelProvider + * @test + */ + public function testSerializationSmokeTest(BlogArticle $blogArticle) + { + // Mock the HTTP call that's made for subjects. + $this->mockSubjects(); + + // Check A to B + $serialized = $this->callSerialize($this->indexer, $blogArticle); + /** @var BlogArticle $deserialized */ + $deserialized = $this->callDeserialize($this->indexer, $serialized); + $this->assertInstanceOf(BlogArticle::class, $deserialized); + // Check B to A + $final_serialized = $this->callSerialize($this->indexer, $deserialized); + $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testIndexOfBlogArticle(BlogArticle $blogArticle) + { + $changeSet = $this->indexer->prepareChangeSet($blogArticle); + + $this->assertCount(0, $changeSet->getDeletes()); + $this->assertCount(1, $changeSet->getInserts()); + $insert = $changeSet->getInserts()[0]; + $article = $insert['json']; + $id = $insert['id']; + + $this->assertJson($article, 'Article is not valid JSON'); + $this->assertNotNull($id, 'An ID is required.'); + $this->assertStringStartsWith('blog-article-', $id, 'ID should be assigned an appropriate prefix.'); + } +} diff --git a/tests/src/Search/Indexer/ModelIndexer/CallSerializer.php b/tests/src/Search/Indexer/ModelIndexer/CallSerializer.php new file mode 100644 index 00000000..ac16565b --- /dev/null +++ b/tests/src/Search/Indexer/ModelIndexer/CallSerializer.php @@ -0,0 +1,28 @@ +setAccessible(true); + $value = $method->invoke($indexer, $entity); + $method->setAccessible(false); + return $value; + } + + public function callDeserialize(ModelIndexer $indexer, $entity) + { + $method = new ReflectionMethod($indexer, 'deserialize'); + $method->setAccessible(true); + $value = $method->invoke($indexer, $entity); + $method->setAccessible(false); + return $value; + } +} diff --git a/tests/src/Search/Indexer/ModelIndexer/CollectionIndexerTest.php b/tests/src/Search/Indexer/ModelIndexer/CollectionIndexerTest.php new file mode 100644 index 00000000..6ba20459 --- /dev/null +++ b/tests/src/Search/Indexer/ModelIndexer/CollectionIndexerTest.php @@ -0,0 +1,70 @@ +indexer = new CollectionIndexer($this->getSerializer()); + } + + protected function getModelDefinitions() + { + return [ + ['model' => 'collection', 'modelClass' => Collection::class, 'version' => 2] + ]; + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testSerializationSmokeTest(Collection $collection) + { + // Mock the HTTP call that's made for subjects. + $this->mockSubjects(); + // Check A to B + $serialized = $this->callSerialize($this->indexer, $collection); + /** @var Collection $deserialized */ + $deserialized = $this->callDeserialize($this->indexer, $serialized); + $this->assertInstanceOf(Collection::class, $deserialized); + // Check B to A + $final_serialized = $this->callSerialize($this->indexer, $deserialized); + $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testIndexOfCollection(Collection $collection) + { + $changeSet = $this->indexer->prepareChangeSet($collection); + + $this->assertCount(0, $changeSet->getDeletes()); + $this->assertCount(1, $changeSet->getInserts()); + + $insert = $changeSet->getInserts()[0]; + $article = $insert['json']; + $id = $insert['id']; + $this->assertJson($article, 'Collection is not valid JSON'); + $this->assertNotNull($id, 'An ID is required.'); + $this->assertStringStartsWith('collection-', $id, 'ID should be assigned an appropriate prefix.'); + } +} diff --git a/tests/src/Search/Workflow/GetSerializer.php b/tests/src/Search/Indexer/ModelIndexer/GetSerializer.php similarity index 89% rename from tests/src/Search/Workflow/GetSerializer.php rename to tests/src/Search/Indexer/ModelIndexer/GetSerializer.php index 5d787109..1430f857 100644 --- a/tests/src/Search/Workflow/GetSerializer.php +++ b/tests/src/Search/Indexer/ModelIndexer/GetSerializer.php @@ -1,6 +1,6 @@ indexer = new InterviewIndexer($this->getSerializer()); + } + + protected function getModelDefinitions() + { + return [ + ['model' => 'interview', 'modelClass' => Interview::class, 'version' => 1] + ]; + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testSerializationSmokeTest(Interview $interview) + { + // Mock the HTTP call that's made for subjects. + $this->mockSubjects(); + // Check A to B + $serialized = $this->callSerialize($this->indexer, $interview); + /** @var Interview $deserialized */ + $deserialized = $this->callDeserialize($this->indexer, $serialized); + $this->assertInstanceOf(Interview::class, $deserialized); + // Check B to A + $final_serialized = $this->callSerialize($this->indexer, $deserialized); + $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testIndexOfInterview(Interview $interview) + { + $changeSet = $this->indexer->prepareChangeSet($interview); + + $this->assertCount(0, $changeSet->getDeletes()); + $this->assertCount(1, $changeSet->getInserts()); + + $insert = $changeSet->getInserts()[0]; + $article = $insert['json']; + $id = $insert['id']; + $this->assertJson($article, 'Interview is not valid JSON'); + $this->assertNotNull($id, 'An ID is required.'); + $this->assertStringStartsWith('interview-', $id, 'ID should be assigned an appropriate prefix.'); + } +} diff --git a/tests/src/Search/Indexer/ModelIndexer/LabsPostIndexerTest.php b/tests/src/Search/Indexer/ModelIndexer/LabsPostIndexerTest.php new file mode 100644 index 00000000..108741f9 --- /dev/null +++ b/tests/src/Search/Indexer/ModelIndexer/LabsPostIndexerTest.php @@ -0,0 +1,70 @@ +indexer = new LabsPostIndexer($this->getSerializer()); + } + + protected function getModelDefinitions() + { + return [ + ['model' => 'labs-post', 'modelClass' => LabsPost::class, 'version' => 1] + ]; + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testSerializationSmokeTest(LabsPost $labsPost) + { + // Mock the HTTP call that's made for subjects. + $this->mockSubjects(); + // Check A to B + $serialized = $this->callSerialize($this->indexer, $labsPost); + /** @var LabsPost $deserialized */ + $deserialized = $this->callDeserialize($this->indexer, $serialized); + $this->assertInstanceOf(LabsPost::class, $deserialized); + // Check B to A + $final_serialized = $this->callSerialize($this->indexer, $deserialized); + $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testIndexOfLabsPost(LabsPost $labsPost) + { + $changeSet = $this->indexer->prepareChangeSet($labsPost); + + $this->assertCount(0, $changeSet->getDeletes()); + $this->assertCount(1, $changeSet->getInserts()); + + $insert = $changeSet->getInserts()[0]; + $article = $insert['json']; + $id = $insert['id']; + $this->assertJson($article, 'LabsPost is not valid JSON'); + $this->assertNotNull($id, 'An ID is required.'); + $this->assertStringStartsWith('labs-post-', $id, 'ID should be assigned an appropriate prefix.'); + } +} diff --git a/tests/src/Search/Indexer/ModelIndexer/ModelProvider.php b/tests/src/Search/Indexer/ModelIndexer/ModelProvider.php new file mode 100644 index 00000000..f29e5ae9 --- /dev/null +++ b/tests/src/Search/Indexer/ModelIndexer/ModelProvider.php @@ -0,0 +1,46 @@ +getModelDefinitions() as $modelDefinition) { + $model = $modelDefinition['model']; + $version = $modelDefinition['version']; + $modelClass = $modelDefinition['modelClass']; + + $paths[] = ComposerLocator::getPath('elife/api') . "/dist/samples/{$model}/v{$version}"; + + // Collect local fixtures + if (is_dir($localPath = __DIR__ . "/../../../Fixtures/{$model}/v{$version}")) { + $paths[] = $localPath; + } + + $finder = new Finder(); + + $finder->files()->in($paths)->name('*.json'); + + // Iterate over files found by Finder + foreach ($finder as $file) { + $name = "{$model}/v{$version}/{$file->getBasename()}"; + $contents = json_decode($file->getContents(), true); + $object = $this->getSerializer()->denormalize($contents, $modelClass); + yield $name => [$object]; + } + } + } +} diff --git a/tests/src/Search/Indexer/ModelIndexer/PodcastEpisodeIndexerTest.php b/tests/src/Search/Indexer/ModelIndexer/PodcastEpisodeIndexerTest.php new file mode 100644 index 00000000..248fe912 --- /dev/null +++ b/tests/src/Search/Indexer/ModelIndexer/PodcastEpisodeIndexerTest.php @@ -0,0 +1,70 @@ +indexer = new PodcastEpisodeIndexer($this->getSerializer()); + } + + protected function getModelDefinitions() + { + return [ + ['model' => 'podcast-episode', 'modelClass' => PodcastEpisode::class, 'version' => 1] + ]; + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testSerializationSmokeTest(PodcastEpisode $podcastEpisode) + { + // Mock the HTTP call that's made for subjects. + $this->mockSubjects(); + // Check A to B + $serialized = $this->callSerialize($this->indexer, $podcastEpisode); + /** @var PodcastEpisode $deserialized */ + $deserialized = $this->callDeserialize($this->indexer, $serialized); + $this->assertInstanceOf(PodcastEpisode::class, $deserialized); + // Check B to A + $final_serialized = $this->callSerialize($this->indexer, $deserialized); + $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testIndexOfPodcastEpisode(PodcastEpisode $podcastEpisode) + { + $changeSet = $this->indexer->prepareChangeSet($podcastEpisode); + + $this->assertCount(0, $changeSet->getDeletes()); + $this->assertCount(1, $changeSet->getInserts()); + + $insert = $changeSet->getInserts()[0]; + $article = $insert['json']; + $id = $insert['id']; + $this->assertJson($article, 'PodcastEpisode is not valid JSON'); + $this->assertNotNull($id, 'An ID is required.'); + $this->assertStringStartsWith('podcast-episode-', $id, 'ID should be assigned an appropriate prefix.'); + } +} diff --git a/tests/src/Search/Indexer/ModelIndexer/ResearchArticleIndexerTest.php b/tests/src/Search/Indexer/ModelIndexer/ResearchArticleIndexerTest.php new file mode 100644 index 00000000..4066ae3c --- /dev/null +++ b/tests/src/Search/Indexer/ModelIndexer/ResearchArticleIndexerTest.php @@ -0,0 +1,156 @@ +elastic = Mockery::mock(MappedElasticsearchClient::class); + $this->indexer = new ResearchArticleIndexer($this->getSerializer(), []); + } + + protected function getModelDefinitions() + { + return [ + ['model' => 'article-vor', 'modelClass' => ArticleVoR::class, 'version' => 8], + ['model' => 'article-poa', 'modelClass' => ArticlePoA::class, 'version' => 4], + ]; + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testSerializationSmokeTest(ArticleVersion $researchArticle) + { + // Mock the HTTP call that's made for subjects. + $this->mockSubjects(); + // Check A to B + $serialized = $this->callSerialize($this->indexer, $researchArticle); + /** @var ArticlePoA $deserialized */ + $deserialized = $this->callDeserialize($this->indexer, $serialized); + $this->assertInstanceOf(ArticleVersion::class, $deserialized); + // Check B to A + $final_serialized = $this->callSerialize($this->indexer, $deserialized); + $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testIndexOfResearchArticle(ArticleVersion $researchArticle) + { + $changeSet = $this->indexer->prepareChangeSet($researchArticle); + + $this->assertCount(1, $changeSet->getInserts()); + + $insert = $changeSet->getInserts()[0]; + $article = $insert['json']; + $id = $insert['id']; + $this->assertJson($article, 'Article is not valid JSON'); + $this->assertNotNull($id, 'An ID is required.'); + $this->assertStringStartsWith('research-article-', $id, 'ID should be assigned an appropriate prefix.'); + + if ($researchArticle instanceof ArticleVoR) { + $this->assertCount(1, $changeSet->getDeletes()); + $delete = $changeSet->getDeletes()[0]; + $this->assertStringStartsWith('reviewed-preprint-', $delete, 'The ID of the delete should be assigned an appropriate prefix.'); + } else { + $this->assertCount(0, $changeSet->getDeletes()); + } + } + + public function testStatusDateIsUsedAsTheSortDateWhenThereIsNoRdsArticle() + { + $indexer = new ResearchArticleIndexer( + $this->getSerializer(), + ['article-2' => ['date' => '2020-09-08T07:06:05Z']] + ); + + $article = $this->getArticle(); + $changeSet = $indexer->prepareChangeSet($article); + + $return = json_decode($changeSet->getInserts()[0]['json'], true); + + $this->assertSame('2010-02-03T04:05:06Z', $return['sortDate']); + } + + public function testRdsDateIsUsedAsTheSortDateWhenThereIsAnRdsArticle() + { + $indexer = new ResearchArticleIndexer( + $this->getSerializer(), + ['article-2' => ['date' => '2020-09-08T07:06:05Z']] + ); + + $article = $this->getArticle(2); + $changeSet = $indexer->prepareChangeSet($article); + + $return = json_decode($changeSet->getInserts()[0]['json'], true); + + $this->assertSame('2020-09-08T07:06:05Z', $return['sortDate']); + } + + private function getArticle($id = 1, $status = 'poa') + { + $sanitisedStatus = ($status === 'vor') ? 'vor' : 'poa'; + + return $this->getSerializer()->denormalize(array_filter([ + 'id' => 'article-'.$id, + 'stage' => 'published', + 'version' => 4, + 'type' => 'research-article', + 'doi' => 'DOI', + 'title' => 'title', + 'statusDate' => '2010-02-03T04:05:06Z', + 'reviewedDate' => '2020-09-08T07:06:05Z', + 'curationLabels' => ['foo', 'bar'], + 'volume' => 1, + 'elocationId' => 'elocationId', + 'copyright' => [ + 'license' => 'license', + 'statement' => 'statement', + ], + 'body' => ($sanitisedStatus === 'vor') ? [ + [ + "type" => "section", + "id" => "s-1", + "title" => "Introduction", + "content" => [ + [ + "type" => "paragraph", + "text" => "Introduction text." + ] + ] + ] + ] : null, + 'status' => $sanitisedStatus, + ]), ($sanitisedStatus === 'vor') ? ArticleVoR::class : ArticlePoA::class); + } +} diff --git a/tests/src/Search/Indexer/ModelIndexer/ReviewedPreprintIndexerTest.php b/tests/src/Search/Indexer/ModelIndexer/ReviewedPreprintIndexerTest.php new file mode 100644 index 00000000..2c347e23 --- /dev/null +++ b/tests/src/Search/Indexer/ModelIndexer/ReviewedPreprintIndexerTest.php @@ -0,0 +1,126 @@ +elastic = Mockery::mock(MappedElasticsearchClient::class); + $this->indexer = new ReviewedPreprintIndexer($this->getSerializer(), $this->elastic); + } + + protected function getModelDefinitions() + { + return [ + ['model' => 'reviewed-preprint', 'modelClass' => ReviewedPreprint::class, 'version' => 1] + ]; + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testSerializationSmokeTest(ReviewedPreprint $reviewedPreprint) + { + // Mock the HTTP call that's made for subjects. + $this->mockSubjects(); + // Check A to B + $serialized = $this->callSerialize($this->indexer, $reviewedPreprint); + /** @var ReviewedPreprint $deserialized */ + $deserialized = $this->callDeserialize($this->indexer, $serialized); + $this->assertInstanceOf(ReviewedPreprint::class, $deserialized); + // Check B to A + $final_serialized = $this->callSerialize($this->indexer, $deserialized); + $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testIndexOfPodcastEpisode(ReviewedPreprint $reviewedPreprint) + { + $this->elastic->shouldReceive('getDocumentById') + ->with('research-article-'.$reviewedPreprint->getId(), null, true) + ->andReturn(null); + $this->elastic->shouldReceive('getDocumentById') + ->with('tools-resources-'.$reviewedPreprint->getId(), null, true) + ->andReturn(null); + $this->elastic->shouldReceive('getDocumentById') + ->with('short-report-'.$reviewedPreprint->getId(), null, true) + ->andReturn(null); + $this->elastic->shouldReceive('getDocumentById') + ->with('research-advance-'.$reviewedPreprint->getId(), null, true) + ->andReturn(null); + $changeSet = $this->indexer->prepareChangeSet($reviewedPreprint); + + $this->assertCount(0, $changeSet->getDeletes()); + $this->assertCount(1, $changeSet->getInserts()); + + $insert = $changeSet->getInserts()[0]; + $article = $insert['json']; + $id = $insert['id']; + $this->assertJson($article, 'Article is not valid JSON'); + $this->assertNotNull($id, 'An ID is required.'); + $this->assertStringStartsWith('reviewed-preprint-', $id, 'ID should be assigned an appropriate prefix.'); + } + + + /** + * @dataProvider modelProvider + * @test + */ + public function testIndexOfReviewedPreprintSkipped(ReviewedPreprint $reviewedPreprint) + { + $this->elastic->shouldReceive('getDocumentById') + ->with('research-article-'.$reviewedPreprint->getId(), null, true) + ->andReturn('found'); + + $changeSet = $this->indexer->prepareChangeSet($reviewedPreprint); + + $this->assertCount(0, $changeSet->getDeletes()); + $this->assertCount(0, $changeSet->getInserts()); + } + + /** + * @dataProvider modelProvider + * @test + */ + public function testIndexOfReviewedPreprintSkippedToolsResources(ReviewedPreprint $reviewedPreprint) + { + $this->elastic->shouldReceive('getDocumentById') + ->with('research-article-'.$reviewedPreprint->getId(), null, true) + ->andReturn(null); + $this->elastic->shouldReceive('getDocumentById') + ->with('tools-resources-'.$reviewedPreprint->getId(), null, true) + ->andReturn('found'); + + $changeSet = $this->indexer->prepareChangeSet($reviewedPreprint); + + $this->assertCount(0, $changeSet->getDeletes()); + $this->assertCount(0, $changeSet->getInserts()); + } +} diff --git a/tests/src/Search/Workflow/BlogArticleWorkflowTest.php b/tests/src/Search/Workflow/BlogArticleWorkflowTest.php deleted file mode 100644 index ed85a378..00000000 --- a/tests/src/Search/Workflow/BlogArticleWorkflowTest.php +++ /dev/null @@ -1,131 +0,0 @@ -mockSubjects(); - // Check A to B - $serialized = $this->workflow->serialize($blogArticle); - /** @var BlogArticle $deserialized */ - $deserialized = $this->workflow->deserialize($serialized); - $this->assertInstanceOf(BlogArticle::class, $deserialized); - // Check B to A - $final_serialized = $this->workflow->serialize($deserialized); - $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testIndexOfBlogArticle(BlogArticle $blogArticle) - { - $return = $this->workflow->index($blogArticle); - $article = $return['json']; - $id = $return['id']; - $this->assertJson($article, 'Article is not valid JSON'); - $this->assertNotNull($id, 'An ID is required.'); - $this->assertStringStartsWith('blog-article-', $id, 'ID should be assigned an appropriate prefix.'); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testInsertOfBlogArticle(BlogArticle $blogArticle) - { - $this->elastic->shouldReceive('indexJsonDocument'); - $ret = $this->workflow->insert($this->workflow->serialize($blogArticle), $blogArticle->getId()); - $this->assertArrayHasKey('id', $ret); - $id = $ret['id']; - $this->assertEquals($blogArticle->getId(), $id); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testPostValidateOfBlogArticle(BlogArticle $blogArticle) - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with($blogArticle->getId()) - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andReturn(true); - $ret = $this->workflow->postValidate($blogArticle->getId()); - $this->assertEquals(1, $ret); - } - - /** - * @test - */ - public function testPostValidateOfBlogArticleFailure() - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with('id') - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andThrow(Exception::class); - $this->elastic->shouldReceive('deleteDocument') - ->once() - ->with('id'); - $ret = $this->workflow->postValidate('id'); - $this->assertEquals(-1, $ret); - } -} diff --git a/tests/src/Search/Workflow/CollectionWorkflowTest.php b/tests/src/Search/Workflow/CollectionWorkflowTest.php deleted file mode 100644 index 4a0c5ad4..00000000 --- a/tests/src/Search/Workflow/CollectionWorkflowTest.php +++ /dev/null @@ -1,131 +0,0 @@ -mockSubjects(); - // Check A to B - $serialized = $this->workflow->serialize($collection); - /** @var Collection $deserialized */ - $deserialized = $this->workflow->deserialize($serialized); - $this->assertInstanceOf(Collection::class, $deserialized); - // Check B to A - $final_serialized = $this->workflow->serialize($deserialized); - $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testIndexOfCollection(Collection $collection) - { - $return = $this->workflow->index($collection); - $article = $return['json']; - $id = $return['id']; - $this->assertJson($article, 'Collection is not valid JSON'); - $this->assertNotNull($id, 'An ID is required.'); - $this->assertStringStartsWith('collection-', $id, 'ID should be assigned an appropriate prefix.'); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testInsertOfCollection(Collection $collection) - { - $this->elastic->shouldReceive('indexJsonDocument'); - $ret = $this->workflow->insert($this->workflow->serialize($collection), $collection->getId()); - $this->assertArrayHasKey('id', $ret); - $id = $ret['id']; - $this->assertEquals($collection->getId(), $id); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testPostValidateOfCollection(Collection $collection) - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with($collection->getId()) - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andReturn(true); - $ret = $this->workflow->postValidate($collection->getId()); - $this->assertEquals(1, $ret); - } - - /** - * @test - */ - public function testPostValidateOfCollectionFailure() - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with('id') - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andThrow(Exception::class); - $this->elastic->shouldReceive('deleteDocument') - ->once() - ->with('id'); - $ret = $this->workflow->postValidate('id'); - $this->assertEquals(-1, $ret); - } -} diff --git a/tests/src/Search/Workflow/GetValidator.php b/tests/src/Search/Workflow/GetValidator.php deleted file mode 100644 index f8aa4d37..00000000 --- a/tests/src/Search/Workflow/GetValidator.php +++ /dev/null @@ -1,45 +0,0 @@ -configureListeners(function (EventDispatcher $dispatcher) { - $dispatcher->addSubscriber(new ElasticsearchDiscriminator()); - }) - ->build(); - - // PSR-7 Bridge - $psr17Factory = new Psr17Factory(); - - return new ApiValidator( - $serializer, - SerializationContext::create(), - new JsonMessageValidator( - new PathBasedSchemaFinder(ComposerLocator::getPath('elife/api').'/dist/model'), - new Validator() - ), - new PsrHttpFactory( - $psr17Factory, - $psr17Factory, - $psr17Factory, - $psr17Factory - ) - ); - } -} diff --git a/tests/src/Search/Workflow/InterviewWorkflowTest.php b/tests/src/Search/Workflow/InterviewWorkflowTest.php deleted file mode 100644 index fcd4baf9..00000000 --- a/tests/src/Search/Workflow/InterviewWorkflowTest.php +++ /dev/null @@ -1,131 +0,0 @@ -mockSubjects(); - // Check A to B - $serialized = $this->workflow->serialize($interview); - /** @var Interview $deserialized */ - $deserialized = $this->workflow->deserialize($serialized); - $this->assertInstanceOf(Interview::class, $deserialized); - // Check B to A - $final_serialized = $this->workflow->serialize($deserialized); - $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testIndexOfInterview(Interview $interview) - { - $return = $this->workflow->index($interview); - $article = $return['json']; - $id = $return['id']; - $this->assertJson($article, 'Interview is not valid JSON'); - $this->assertNotNull($id, 'An ID is required.'); - $this->assertStringStartsWith('interview-', $id, 'ID should be assigned an appropriate prefix.'); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testInsertOfInterview(Interview $interview) - { - $this->elastic->shouldReceive('indexJsonDocument'); - $ret = $this->workflow->insert($this->workflow->serialize($interview), $interview->getId()); - $this->assertArrayHasKey('id', $ret); - $id = $ret['id']; - $this->assertEquals($interview->getId(), $id); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testPostValidateOfInterview(Interview $interview) - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with($interview->getId()) - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andReturn(true); - $ret = $this->workflow->postValidate($interview->getId()); - $this->assertEquals(1, $ret); - } - - /** - * @test - */ - public function testPostValidateOfInterviewFailure() - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with('id') - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andThrow(Exception::class); - $this->elastic->shouldReceive('deleteDocument') - ->once() - ->with('id'); - $ret = $this->workflow->postValidate('id'); - $this->assertEquals(-1, $ret); - } -} diff --git a/tests/src/Search/Workflow/LabsPostWorkflowTest.php b/tests/src/Search/Workflow/LabsPostWorkflowTest.php deleted file mode 100644 index 34149602..00000000 --- a/tests/src/Search/Workflow/LabsPostWorkflowTest.php +++ /dev/null @@ -1,131 +0,0 @@ -mockSubjects(); - // Check A to B - $serialized = $this->workflow->serialize($labsPost); - /** @var LabsPost $deserialized */ - $deserialized = $this->workflow->deserialize($serialized); - $this->assertInstanceOf(LabsPost::class, $deserialized); - // Check B to A - $final_serialized = $this->workflow->serialize($deserialized); - $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testIndexOfLabsPost(LabsPost $labsPost) - { - $return = $this->workflow->index($labsPost); - $article = $return['json']; - $id = $return['id']; - $this->assertJson($article, 'LabsPost is not valid JSON'); - $this->assertNotNull($id, 'An ID is required.'); - $this->assertStringStartsWith('labs-post-', $id, 'ID should be assigned an appropriate prefix.'); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testInsertOfLabsPost(LabsPost $labsPost) - { - $this->elastic->shouldReceive('indexJsonDocument'); - $ret = $this->workflow->insert($this->workflow->serialize($labsPost), $labsPost->getId()); - $this->assertArrayHasKey('id', $ret); - $id = $ret['id']; - $this->assertEquals($labsPost->getId(), $id); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testPostValidateOfLabsPost(LabsPost $labsPost) - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with($labsPost->getId()) - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andReturn(true); - $ret = $this->workflow->postValidate($labsPost->getId()); - $this->assertEquals(1, $ret); - } - - /** - * @test - */ - public function testPostValidateOfLabsPostFailure() - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with('id') - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andThrow(Exception::class); - $this->elastic->shouldReceive('deleteDocument') - ->once() - ->with('id'); - $ret = $this->workflow->postValidate('id'); - $this->assertEquals(-1, $ret); - } -} diff --git a/tests/src/Search/Workflow/PodcastEpisodeWorkflowTest.php b/tests/src/Search/Workflow/PodcastEpisodeWorkflowTest.php deleted file mode 100644 index 54031cd4..00000000 --- a/tests/src/Search/Workflow/PodcastEpisodeWorkflowTest.php +++ /dev/null @@ -1,131 +0,0 @@ -mockSubjects(); - // Check A to B - $serialized = $this->workflow->serialize($podcastEpisode); - /** @var PodcastEpisode $deserialized */ - $deserialized = $this->workflow->deserialize($serialized); - $this->assertInstanceOf(PodcastEpisode::class, $deserialized); - // Check B to A - $final_serialized = $this->workflow->serialize($deserialized); - $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testIndexOfPodcastEpisode(PodcastEpisode $podcastEpisode) - { - $return = $this->workflow->index($podcastEpisode); - $article = $return['json']; - $id = $return['id']; - $this->assertJson($article, 'PodcastEpisode is not valid JSON'); - $this->assertNotNull($id, 'An ID is required.'); - $this->assertStringStartsWith('podcast-episode-', $id, 'ID should be assigned an appropriate prefix.'); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testInsertOfPodcastEpisode(PodcastEpisode $podcastEpisode) - { - $this->elastic->shouldReceive('indexJsonDocument'); - $ret = $this->workflow->insert($this->workflow->serialize($podcastEpisode), $podcastEpisode->getNumber()); - $this->assertArrayHasKey('id', $ret); - $id = $ret['id']; - $this->assertEquals($podcastEpisode->getNumber(), $id); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testPostValidateOfPodcastEpisode(PodcastEpisode $podcastEpisode) - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with($podcastEpisode->getNumber()) - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andReturn(true); - $ret = $this->workflow->postValidate($podcastEpisode->getNumber()); - $this->assertEquals(1, $ret); - } - - /** - * @test - */ - public function testPostValidateOfPodcastEpisodeFailure() - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with(1) - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andThrow(Exception::class); - $this->elastic->shouldReceive('deleteDocument') - ->once() - ->with(1); - $ret = $this->workflow->postValidate(1); - $this->assertEquals(-1, $ret); - } -} diff --git a/tests/src/Search/Workflow/ResearchArticleWorkflowTest.php b/tests/src/Search/Workflow/ResearchArticleWorkflowTest.php deleted file mode 100644 index 565683fc..00000000 --- a/tests/src/Search/Workflow/ResearchArticleWorkflowTest.php +++ /dev/null @@ -1,193 +0,0 @@ -mockSubjects(); - // Check A to B - $serialized = $this->workflow->serialize($researchArticle); - /** @var ArticlePoA $deserialized */ - $deserialized = $this->workflow->deserialize($serialized); - $this->assertInstanceOf(ArticleVersion::class, $deserialized); - // Check B to A - $final_serialized = $this->workflow->serialize($deserialized); - $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testIndexOfResearchArticle(ArticleVersion $researchArticle) - { - $this->elastic->shouldReceive('deleteDocument'); - $return = $this->workflow->index($researchArticle); - $article = $return['json']; - $id = $return['id']; - $this->assertJson($article, 'Article is not valid JSON'); - $this->assertNotNull($id, 'An ID is required.'); - $this->assertStringStartsWith('research-article-', $id, 'ID should be assigned an appropriate prefix.'); - } - - public function testStatusDateIsUsedAsTheSortDateWhenThereIsNoRdsArticle() - { - $this->workflow = new ResearchArticleWorkflow($this->getSerializer(), new ExceptionNullLogger(), - $this->elastic, $this->validator, ['article-2' => ['date' => '2020-09-08T07:06:05Z']]); - - $article = $this->getArticle(); - - $return = json_decode($this->workflow->index($article)['json'], true); - - $this->assertSame('2010-02-03T04:05:06Z', $return['sortDate']); - } - - public function testRdsDateIsUsedAsTheSortDateWhenThereIsAnRdsArticle() - { - $this->workflow = new ResearchArticleWorkflow($this->getSerializer(), new ExceptionNullLogger(), - $this->elastic, $this->validator, ['article-2' => ['date' => '2020-09-08T07:06:05Z']]); - - $article = $this->getArticle(2); - - $return = json_decode($this->workflow->index($article)['json'], true); - - $this->assertSame('2020-09-08T07:06:05Z', $return['sortDate']); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testInsertOfResearchArticle(ArticleVersion $researchArticle) - { - // TODO: this should set up an expectation about actual ArticlePoA data being received, as passing in a BlogArticle doesn't break the test - $this->elastic->shouldReceive('indexJsonDocument'); - $ret = $this->workflow->insert($this->workflow->serialize($researchArticle), $researchArticle->getId()); - $this->assertArrayHasKey('id', $ret); - $id = $ret['id']; - $this->assertEquals($researchArticle->getId(), $id); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testPostValidateOfResearchArticle(ArticleVersion $researchArticle) - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with($researchArticle->getId()) - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andReturn(true); - $ret = $this->workflow->postValidate($researchArticle->getId()); - $this->assertEquals(1, $ret); - } - - /** - * @test - */ - public function testPostValidateOfResearchArticleFailure() - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with('id') - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andThrow(Exception::class); - $this->elastic->shouldReceive('deleteDocument') - ->once() - ->with('id'); - $ret = $this->workflow->postValidate('id'); - $this->assertEquals(-1, $ret); - } - - public function workflowProvider(string $model = null, string $modelClass = null, int $version = null) : Traversable - { - foreach (array_merge( - iterator_to_array(parent::workflowProvider('article-vor', ArticleVoR::class, 8)), - iterator_to_array(parent::workflowProvider('article-poa', ArticlePoA::class, 4)) - ) as $k => $v) { - yield $k => $v; - } - } - - private function getArticle($id = 1, $status = 'poa') - { - $sanitisedStatus = ($status === 'vor') ? 'vor' : 'poa'; - - return $this->getSerializer()->denormalize(array_filter([ - 'id' => 'article-'.$id, - 'stage' => 'published', - 'version' => 4, - 'type' => 'research-article', - 'doi' => 'DOI', - 'title' => 'title', - 'statusDate' => '2010-02-03T04:05:06Z', - 'reviewedDate' => '2020-09-08T07:06:05Z', - 'curationLabels' => ['foo', 'bar'], - 'volume' => 1, - 'elocationId' => 'elocationId', - 'copyright' => [ - 'license' => 'license', - 'statement' => 'statement', - ], - 'body' => ($sanitisedStatus === 'vor') ? [ - [ - "type" => "section", - "id" => "s-1", - "title" => "Introduction", - "content" => [ - [ - "type" => "paragraph", - "text" => "Introduction text." - ] - ] - ] - ] : null, - 'status' => $sanitisedStatus, - ]), ($sanitisedStatus === 'vor') ? ArticleVoR::class : ArticlePoA::class); - } -} diff --git a/tests/src/Search/Workflow/ReviewedPreprintWorkflowTest.php b/tests/src/Search/Workflow/ReviewedPreprintWorkflowTest.php deleted file mode 100644 index c726726f..00000000 --- a/tests/src/Search/Workflow/ReviewedPreprintWorkflowTest.php +++ /dev/null @@ -1,181 +0,0 @@ -mockSubjects(); - // Check A to B - $serialized = $this->workflow->serialize($reviewedPreprint); - /** @var ReviewedPreprint $deserialized */ - $deserialized = $this->workflow->deserialize($serialized); - $this->assertInstanceOf(ReviewedPreprint::class, $deserialized); - // Check B to A - $final_serialized = $this->workflow->serialize($deserialized); - $this->assertJsonStringEqualsJsonString($serialized, $final_serialized); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testIndexOfReviewedPreprint(ReviewedPreprint $reviewedPreprint) - { - $this->elastic->shouldReceive('getDocumentById') - ->with('research-article-'.$reviewedPreprint->getId(), null, true) - ->andReturn(null); - $this->elastic->shouldReceive('getDocumentById') - ->with('tools-resources-'.$reviewedPreprint->getId(), null, true) - ->andReturn(null); - $this->elastic->shouldReceive('getDocumentById') - ->with('short-report-'.$reviewedPreprint->getId(), null, true) - ->andReturn(null); - $this->elastic->shouldReceive('getDocumentById') - ->with('research-advance-'.$reviewedPreprint->getId(), null, true) - ->andReturn(null); - $return = $this->workflow->index($reviewedPreprint); - $article = $return['json']; - $id = $return['id']; - $this->assertJson($article, 'Article is not valid JSON'); - $this->assertNotNull($id, 'An ID is required.'); - $this->assertStringStartsWith('reviewed-preprint-', $id, 'ID should be assigned an appropriate prefix.'); - $this->assertFalse($return['skipInsert']); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testIndexOfReviewedPreprintSkipped(ReviewedPreprint $reviewedPreprint) - { - $this->elastic->shouldReceive('getDocumentById') - ->with('research-article-'.$reviewedPreprint->getId(), null, true) - ->andReturn('found'); - - $this->assertSame([ - 'json' => '', - 'id' => $reviewedPreprint->getId(), - 'skipInsert' => true, - ], $this->workflow->index($reviewedPreprint)); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testIndexOfReviewedPreprintSkippedToolsResources(ReviewedPreprint $reviewedPreprint) - { - $this->elastic->shouldReceive('getDocumentById') - ->with('research-article-'.$reviewedPreprint->getId(), null, true) - ->andReturn(null); - $this->elastic->shouldReceive('getDocumentById') - ->with('tools-resources-'.$reviewedPreprint->getId(), null, true) - ->andReturn('found'); - - $this->assertSame([ - 'json' => '', - 'id' => $reviewedPreprint->getId(), - 'skipInsert' => true, - ], $this->workflow->index($reviewedPreprint)); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testInsertOfReviewedPreprint(ReviewedPreprint $reviewedPreprint) - { - $this->elastic->shouldReceive('indexJsonDocument'); - $ret = $this->workflow->insert($this->workflow->serialize($reviewedPreprint), $reviewedPreprint->getId(), false); - $this->assertArrayHasKey('id', $ret); - $id = $ret['id']; - $this->assertEquals($reviewedPreprint->getId(), $id); - } - - /** - * @dataProvider workflowProvider - * @test - */ - public function testPostValidateOfReviewedPreprint(ReviewedPreprint $reviewedPreprint) - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with($reviewedPreprint->getId()) - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andReturn(true); - $ret = $this->workflow->postValidate($reviewedPreprint->getId(), false); - $this->assertEquals(1, $ret); - } - - /** - * @test - */ - public function testPostValidateOfResearchArticleFailure() - { - $document = Mockery::mock(IsDocumentResponse::class); - $this->elastic->shouldReceive('getDocumentById') - ->once() - ->with('id') - ->andReturn($document); - $document->shouldReceive('unwrap') - ->once() - ->andReturn([]); - $this->validator->shouldReceive('validateSearchResult') - ->once() - ->andThrow(Exception::class); - $this->elastic->shouldReceive('deleteDocument') - ->once() - ->with('id'); - $ret = $this->workflow->postValidate('id', false); - $this->assertEquals(-1, $ret); - } -} diff --git a/tests/src/Search/Workflow/WorkflowTestCase.php b/tests/src/Search/Workflow/WorkflowTestCase.php deleted file mode 100644 index 827755c2..00000000 --- a/tests/src/Search/Workflow/WorkflowTestCase.php +++ /dev/null @@ -1,103 +0,0 @@ -elastic = Mockery::mock(MappedElasticsearchClient::class); - - $logger = new ExceptionNullLogger(); - $this->validator = Mockery::mock(HasSearchResultValidator::class); - $this->workflow = $this->setWorkflow( - $this->getSerializer(), - $logger, - $this->elastic, - $this->validator - ); - } - - abstract protected function setWorkflow( - Serializer $serializer, - LoggerInterface $logger, - MappedElasticsearchClient $client, - HasSearchResultValidator $validator - ) : AbstractWorkflow; - - protected function getModel() : ?string - { - return null; - } - - protected function getModelClass() : ?string - { - return null; - } - - protected function getVersion() : ?int - { - return null; - } - - public function asyncTearDown() - { - Mockery::close(); - parent::tearDown(); - } - - public function workflowProvider(string $model = null, string $modelClass = null, int $version = null) : Traversable - { - $paths = []; - $model = $this->getModel() ?? $model; - $version = $this->getVersion() ?? $version; - $modelClass = $this->getModelClass() ?? $modelClass; - - $paths[] = ComposerLocator::getPath('elife/api') . "/dist/samples/{$model}/v{$version}"; - - // Collect local fixtures - if (is_dir($localPath = __DIR__ . "/../../../Fixtures/{$model}/v{$version}")) { - $paths[] = $localPath; - } - - $finder = new Finder(); - - $finder->files()->in($paths)->name('*.json'); - - // Iterate over files found by Finder - foreach ($finder as $file) { - $name = "{$model}/v{$version}/{$file->getBasename()}"; - $contents = json_decode($file->getContents(), true); - $object = $this->getSerializer()->denormalize($contents, $modelClass); - yield $name => [$object]; - } - } -}