diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index e8ed0dc..ae89bee 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -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:
@@ -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
@@ -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
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 9627064..ff9a3a9 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -11,8 +11,11 @@
processIsolation="false"
stopOnFailure="false">
-
- ./tests/
+
+ ./tests/Unit
+
+
+ ./tests/Feature
@@ -20,4 +23,10 @@
./src
+
+
+
diff --git a/src/Engines/MeilisearchEngine.php b/src/Engines/MeilisearchEngine.php
index 8532755..15d7919 100644
--- a/src/Engines/MeilisearchEngine.php
+++ b/src/Engines/MeilisearchEngine.php
@@ -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 ');
}
diff --git a/tests/Feature/FeatureTestCase.php b/tests/Feature/FeatureTestCase.php
new file mode 100644
index 0000000..39107a5
--- /dev/null
+++ b/tests/Feature/FeatureTestCase.php
@@ -0,0 +1,42 @@
+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()) {
+ $index->delete();
+ }
+ });
+ }
+}
diff --git a/tests/Feature/MeilisearchConsoleCommandTest.php b/tests/Feature/MeilisearchConsoleCommandTest.php
new file mode 100644
index 0000000..ce9f1a5
--- /dev/null
+++ b/tests/Feature/MeilisearchConsoleCommandTest.php
@@ -0,0 +1,50 @@
+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);
+ }
+ }
+}
diff --git a/tests/Feature/MeilisearchTest.php b/tests/Feature/MeilisearchTest.php
new file mode 100644
index 0000000..381c7e9
--- /dev/null
+++ b/tests/Feature/MeilisearchTest.php
@@ -0,0 +1,156 @@
+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');
+
+ $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();
+ }
+}
diff --git a/tests/Fixtures/SearchableModel.php b/tests/Fixtures/SearchableModel.php
index 8f06525..3d2f5e6 100644
--- a/tests/Fixtures/SearchableModel.php
+++ b/tests/Fixtures/SearchableModel.php
@@ -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()
{
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 14e8b36..b11e4cf 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -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';
+ }
}
diff --git a/tests/MeilisearchConsoleCommandTest.php b/tests/Unit/MeilisearchConsoleCommandTest.php
similarity index 73%
rename from tests/MeilisearchConsoleCommandTest.php
rename to tests/Unit/MeilisearchConsoleCommandTest.php
index 67066ff..233aef4 100644
--- a/tests/MeilisearchConsoleCommandTest.php
+++ b/tests/Unit/MeilisearchConsoleCommandTest.php
@@ -1,29 +1,22 @@
expectExceptionMessage('Not enough arguments (missing: "name").');
- $this->artisan('scout:index')
- ->execute();
- }
-
/** @test */
public function commandCreatesIndex()
{
$client = $this->mock(Client::class);
- $client->expects('createIndex')->with($indexName = 'testindex', [])->andReturn(m::mock(Indexes::class));
+ $client->expects('createIndex')->with($indexUid = 'testindex', [])->andReturn(m::mock(Indexes::class));
$engineManager = $this->mock(EngineManager::class);
$engineManager->shouldReceive('engine')->with('meilisearch')->andReturn(new MeilisearchEngine(
@@ -31,9 +24,9 @@ public function commandCreatesIndex()
));
$this->artisan('scout:index', [
- 'name' => $indexName,
+ 'name' => $indexUid,
])
- ->expectsOutput('Index "'.$indexName.'" created.')
+ ->expectsOutput('Index "'.$indexUid.'" created.')
->assertExitCode(0)
->run();
}
@@ -44,7 +37,7 @@ public function keyParameterSetsPrimaryKeyOption()
$client = $this->mock(Client::class);
$client
->expects('createIndex')
- ->with($indexName = 'testindex', ['primaryKey' => $testPrimaryKey = 'foobar'])
+ ->with($indexUid = 'testindex', ['primaryKey' => $testPrimaryKey = 'foobar'])
->andReturn(m::mock(Indexes::class));
$engineManager = $this->mock(EngineManager::class);
@@ -53,10 +46,10 @@ public function keyParameterSetsPrimaryKeyOption()
));
$this->artisan('scout:index', [
- 'name' => $indexName,
+ 'name' => $indexUid,
'--key' => $testPrimaryKey,
])
- ->expectsOutput('Index "'.$indexName.'" created.')
+ ->expectsOutput('Index "'.$indexUid.'" created.')
->assertExitCode(0)
->run();
}
@@ -65,7 +58,7 @@ public function keyParameterSetsPrimaryKeyOption()
public function deleteParameterDeletesIndex()
{
$client = $this->mock(Client::class);
- $client->expects('deleteIndex')->with($indexName = 'testindex')->andReturn([]);
+ $client->expects('deleteIndex')->with($indexUid = 'testindex')->andReturn([]);
$engineManager = $this->mock(EngineManager::class);
$engineManager->shouldReceive('engine')->with('meilisearch')->andReturn(new MeilisearchEngine(
@@ -73,10 +66,10 @@ public function deleteParameterDeletesIndex()
));
$this->artisan('scout:index', [
- 'name' => $indexName,
+ 'name' => $indexUid,
'--delete' => true,
])
- ->expectsOutput('Index "'.$indexName.'" deleted.')
+ ->expectsOutput('Index "'.$indexUid.'" deleted.')
->assertExitCode(0)
->run();
}
diff --git a/tests/MeilisearchEngineTest.php b/tests/Unit/MeilisearchEngineTest.php
similarity index 83%
rename from tests/MeilisearchEngineTest.php
rename to tests/Unit/MeilisearchEngineTest.php
index 841a90b..7c570db 100644
--- a/tests/MeilisearchEngineTest.php
+++ b/tests/Unit/MeilisearchEngineTest.php
@@ -1,28 +1,20 @@
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 updateAddsObjectsToIndex()
{
@@ -266,6 +258,62 @@ public function updateEmptySearchableArrayFromSoftDeletedModelDoesNotAddObjectsT
$engine = new MeilisearchEngine($client, true);
$engine->update(Collection::make([new SoftDeleteEmptySearchableModel()]));
}
+
+ /** @test */
+ public function engineForwardsCallsToMeilisearchClient()
+ {
+ $client = m::mock(Client::class);
+ $client->shouldReceive('testMethodOnClient')->once();
+
+ $engine = new MeilisearchEngine($client);
+ $engine->testMethodOnClient();
+ }
+
+ /** @test */
+ public function updatingEmptyEloquentCollectionDoesNothing()
+ {
+ $client = m::mock(Client::class);
+ $engine = new MeilisearchEngine($client);
+ $engine->update(new Collection());
+ $this->assertTrue(true);
+ }
+
+ /** @test */
+ public function performingSearchWithoutCallbackWorks()
+ {
+ $client = m::mock(Client::class);
+ $client->shouldReceive('index')->once()->andReturn($index = m::mock(Indexes::class));
+ $index->shouldReceive('rawSearch')->once()->andReturn([]);
+
+ $engine = new MeilisearchEngine($client);
+ $builder = new Builder(new SearchableModel(), '');
+ $engine->search($builder);
+ }
+
+ /** @test */
+ public function whereConditionsAreApplied()
+ {
+ $builder = new Builder(new SearchableModel(), '');
+ $builder->where('foo', 'bar');
+ $builder->where('key', 'value');
+ $client = m::mock(Client::class);
+ $client->shouldReceive('index')->once()->andReturn($index = m::mock(Indexes::class));
+ $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([
+ 'filters' => 'foo="bar" AND key="value"',
+ 'limit' => $builder->limit,
+ ]))->andReturn([]);
+
+ $engine = new MeilisearchEngine($client);
+ $engine->search($builder);
+ }
+
+ /** @test */
+ public function engineReturnsHitsEntryFromSearchResponse()
+ {
+ $this->assertTrue(3 === resolve(MeilisearchEngine::class)->getTotalCount([
+ 'nbHits' => 3,
+ ]));
+ }
}
class CustomKeySearchableModel extends SearchableModel