Skip to content
This repository has been archived by the owner on May 13, 2021. It is now read-only.

Add feature tests #102

Merged
merged 14 commits into from
Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ jobs:
strategy:
matrix:
php-versions: ['7.2', '7.3', '7.4', '8.0']
env:
MEILISEARCH_KEY: masterKey
name: tests (php ${{ matrix.php-versions }})
runs-on: ubuntu-latest
steps:
Expand All @@ -25,7 +27,9 @@ jobs:
- name: Validate composer.json and composer.lock
run: composer validate
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
run: composer update --prefer-dist --no-progress --no-interaction
- name: MeiliSearch setup with Docker
run: docker run -d -p 7700:7700 getmeili/meilisearch:latest ./meilisearch --master-key=${{ env.MEILISEARCH_KEY }} --no-analytics=true
- name: Run tests
run: composer test

Expand All @@ -41,7 +45,7 @@ jobs:
- name: Validate composer.json and composer.lock
run: composer validate
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
run: composer update --prefer-dist --no-progress --no-interaction
- name: Run linter
env:
PHP_CS_FIXER_IGNORE_ENV: 1
Expand Down
13 changes: 11 additions & 2 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Test Suite">
<directory suffix=".php">./tests/</directory>
<testsuite name="Unit">
<directory suffix=".php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix=".php">./tests/Feature</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
<php>
<!--
<env name="MEILISEARCH_HOST" value="http://localhost:7700" />
<env name="MEILISEARCH_KEY" value="" />
-->
</php>
mmachatschek marked this conversation as resolved.
Show resolved Hide resolved
</phpunit>
2 changes: 1 addition & 1 deletion src/Engines/MeilisearchEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ protected function performSearch(Builder $builder, array $searchParams = [])
protected function filters(Builder $builder)
{
return collect($builder->wheres)->map(function ($value, $key) {
return $key.'='.'"'.$value.'"';
return sprintf('%s="%s"', $key, $value);
})->values()->implode(' AND ');
}

Expand Down
42 changes: 42 additions & 0 deletions tests/Feature/FeatureTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Meilisearch\Scout\Tests\Feature;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use MeiliSearch\Client;
use MeiliSearch\Endpoints\Indexes;
use Meilisearch\Scout\Tests\TestCase;

abstract class FeatureTestCase extends TestCase
{
public function setUp(): void
{
parent::setUp();

Schema::create('searchable_models', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->timestamps();
});

$this->cleanUp();
}

public function tearDown(): void
{
$this->cleanUp();

parent::tearDown();
}

protected function cleanUp(): void
{
collect(resolve(Client::class)->getAllIndexes())->each(function (Indexes $index) {
// Starts with prefix
if (substr($index->getUid(), 0, strlen($this->getPrefix())) === $this->getPrefix()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this condition?
I think we should delete all the indexes without any condition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wanted to make sure that there is no data lost if someone runs the tests with an existing instance. So this just removes the indexes from meilisearch that start with the prefix set in the tests.

https://github.com/meilisearch/meilisearch-laravel-scout/pull/102/files#diff-abf1fa77a19f720052b75bc802df0260157b9064d8c3c12a80794f01e9acc065R37-R38

$index->delete();
}
});
}
}
50 changes: 50 additions & 0 deletions tests/Feature/MeilisearchConsoleCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Meilisearch\Scout\Tests\Feature;

use MeiliSearch\Client;
use MeiliSearch\Exceptions\HTTPRequestException;

class MeilisearchConsoleCommandTest extends FeatureTestCase
{
/** @test */
public function nameArgumentIsRequired()
{
$this->expectExceptionMessage('Not enough arguments (missing: "name").');
$this->artisan('scout:index')
->execute();
}

/** @test */
public function indexCanBeCreatedAndDeleted()
{
$indexUid = $this->getPrefixedIndexUid('testindex');

$this->artisan('scout:index', [
'name' => $indexUid,
])
->expectsOutput('Index "'.$indexUid.'" created.')
->assertExitCode(0)
->run();

$indexResponse = resolve(Client::class)->index($indexUid)->fetchRawInfo();

$this->assertIsArray($indexResponse);
$this->assertSame($indexUid, $indexResponse['uid']);

$this->artisan('scout:index', [
'name' => $indexUid,
'--delete' => true,
])
->expectsOutput('Index "'.$indexUid.'" deleted.')
->assertExitCode(0)
->run();

try {
resolve(Client::class)->index($indexUid)->fetchRawInfo();
$this->fail('Exception should be thrown that index doesn\'t exist!');
} catch (HTTPRequestException $exception) {
$this->assertTrue(true);
}
}
}
156 changes: 156 additions & 0 deletions tests/Feature/MeilisearchTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php

namespace Meilisearch\Scout\Tests\Feature;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\WithFaker;
use Laravel\Scout\EngineManager;
use MeiliSearch\Client;
use Meilisearch\Scout\Engines\MeilisearchEngine;
use Meilisearch\Scout\Tests\Fixtures\SearchableModel as BaseSearchableModel;

class MeilisearchTest extends FeatureTestCase
{
use WithFaker;

/** @test */
public function clientAndEngineCanBeResolved()
{
$this->assertInstanceOf(Client::class, resolve(Client::class));
$this->assertInstanceOf(EngineManager::class, resolve(EngineManager::class));
$this->assertInstanceOf(MeilisearchEngine::class, resolve(EngineManager::class)->engine('meilisearch'));
}

/** @test */
public function clientCanTalkToMeilisearch()
{
/** @var Client $engine */
$engine = resolve(Client::class);

$this->assertNull($engine->health());
$versionResponse = $engine->version();
$this->assertIsArray($versionResponse);
$this->assertArrayHasKey('commitSha', $versionResponse);
$this->assertArrayHasKey('buildDate', $versionResponse);
$this->assertArrayHasKey('pkgVersion', $versionResponse);
}

/** @test */
public function searchReturnsModels()
{
$model = $this->createSearchableModel('foo');
$this->createSearchableModel('bar');
mmachatschek marked this conversation as resolved.
Show resolved Hide resolved

$this->assertDatabaseCount('searchable_models', 2);

$searchResponse = $this->waitForPendingUpdates($model, function () {
return SearchableModel::search('bar')->raw();
});

$this->assertIsArray($searchResponse);
$this->assertArrayHasKey('hits', $searchResponse);
$this->assertArrayHasKey('query', $searchResponse);
$this->assertTrue(1 === count($searchResponse['hits']));
}

/** @test */
public function searchReturnsCorrectModelAfterUpdate()
{
$fooModel = $this->createSearchableModel('foo');
$this->createSearchableModel('bar');

$this->assertDatabaseCount('searchable_models', 2);

$searchResponse = $this->waitForPendingUpdates($fooModel, function () {
return SearchableModel::search('foo')->raw();
});

$this->assertIsArray($searchResponse);
$this->assertArrayHasKey('hits', $searchResponse);
$this->assertArrayHasKey('query', $searchResponse);
$this->assertTrue(1 === count($searchResponse['hits']));
$this->assertTrue('foo' === $searchResponse['hits'][0]['title']);

$fooModel->update(['title' => 'lorem']);

$searchResponse = $this->waitForPendingUpdates($fooModel, function () {
return SearchableModel::search('lorem')->raw();
});

$this->assertIsArray($searchResponse);
$this->assertArrayHasKey('hits', $searchResponse);
$this->assertArrayHasKey('query', $searchResponse);
$this->assertTrue(1 === count($searchResponse['hits']));
$this->assertTrue('lorem' === $searchResponse['hits'][0]['title']);
}

/** @test */
public function customSearchReturnsResults()
{
$models = $this->createMultipleSearchableModels(10);

$this->assertDatabaseCount('searchable_models', 10);

$searchResponse = $this->waitForPendingUpdates($models->first(), function () {
return SearchableModel::search('', function ($meilisearch, $query, $options) {
$options['limit'] = 2;

return $meilisearch->search($query, $options);
})->raw();
});

$this->assertIsArray($searchResponse);
$this->assertArrayHasKey('hits', $searchResponse);
$this->assertArrayHasKey('query', $searchResponse);
$this->assertTrue(2 === $searchResponse['limit']);
$this->assertTrue(2 === count($searchResponse['hits']));
}

/**
* Fixes race condition and waits some time for the indexation to complete.
*
* @param Model $model
* @param callable $callback
*
* @return mixed
*/
protected function waitForPendingUpdates($model, $callback)
{
$index = resolve(Client::class)->index($model->searchableAs());
$pendingUpdates = $index->getAllUpdateStatus();

foreach ($pendingUpdates as $pendingUpdate) {
if ('processed' !== $pendingUpdate['status']) {
$index->waitForPendingUpdate($pendingUpdate['updateId']);
}
}

return $callback();
}

protected function createMultipleSearchableModels(int $times = 1)
{
$models = collect();

for ($i = 1; $i <= $times; ++$i) {
$models->add($this->createSearchableModel());
}

return $models;
}

protected function createSearchableModel(?string $title = null)
{
return SearchableModel::create([
'title' => $title ?? $this->faker->sentence,
]);
}
}

class SearchableModel extends BaseSearchableModel
{
public function searchableAs()
{
return config('scout.prefix').$this->getTable();
}
}
4 changes: 3 additions & 1 deletion tests/Fixtures/SearchableModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

namespace Meilisearch\Scout\Tests\Fixtures;

use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

class SearchableModel extends Model
{
use Searchable;
use HasTimestamps;

/**
* The attributes that are mass assignable.
*/
protected $fillable = ['id'];
protected $fillable = ['id', 'title'];

public function searchableAs()
{
Expand Down
26 changes: 26 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,30 @@ protected function getPackageProviders($app)
MeilisearchServiceProvider::class,
];
}

protected function getEnvironmentSetUp($app)
{
if (env('DB_CONNECTION')) {
config()->set('database.default', env('DB_CONNECTION'));
} else {
config()->set('database.default', 'testing');
config()->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
}
config()->set('scout.driver', 'meilisearch');
config()->set('scout.prefix', $this->getPrefix());
}

protected function getPrefixedIndexUid(string $indexUid)
{
return sprintf('%s_%s', $this->getPrefix(), $indexUid);
}

protected function getPrefix()
{
return 'meilisearch-laravel-scout_testing';
}
}
Loading