diff --git a/composer.json b/composer.json index bcc2cec0..5f5feb53 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,9 @@ }, "autoload-dev": { "psr-4": { - "Laravel\\Scout\\Tests\\": "tests/" + "Laravel\\Scout\\Tests\\": "tests/", + "Workbench\\App\\": "workbench/app/", + "Workbench\\Database\\Factories\\": "workbench/database/factories/" } }, "extra": { diff --git a/testbench.yaml b/testbench.yaml index 5511d482..39aa6ffe 100644 --- a/testbench.yaml +++ b/testbench.yaml @@ -1,7 +1,9 @@ providers: + - Workbench\App\Providers\WorkbenchServiceProvider - Laravel\Scout\ScoutServiceProvider -migrations: true +migrations: + - workbench/database/migrations workbench: install: true diff --git a/tests/Feature/BuilderTest.php b/tests/Feature/BuilderTest.php index 7d11dab4..66d390e5 100644 --- a/tests/Feature/BuilderTest.php +++ b/tests/Feature/BuilderTest.php @@ -3,43 +3,37 @@ namespace Laravel\Scout\Tests\Feature; use Illuminate\Database\Eloquent\Factories\Sequence; -use Illuminate\Foundation\Auth\User; use Illuminate\Foundation\Testing\LazilyRefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; -use Laravel\Scout\EngineManager; -use Laravel\Scout\Engines\MeilisearchEngine; -use Laravel\Scout\Tests\Fixtures\SearchableUserModel; -use Mockery as m; -use Orchestra\Testbench\Concerns\WithLaravelMigrations; +use Orchestra\Testbench\Attributes\WithConfig; +use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\Concerns\WithWorkbench; -use Orchestra\Testbench\Factories\UserFactory; use Orchestra\Testbench\TestCase; +use Workbench\App\Models\SearchableUser; +use Workbench\Database\Factories\SearchableUserFactory; +#[WithConfig('scout.driver', 'database')] +#[WithMigration] class BuilderTest extends TestCase { - use LazilyRefreshDatabase, WithFaker, WithLaravelMigrations, WithWorkbench; - - protected function defineEnvironment($app) - { - $app->make('config')->set('scout.driver', 'fake'); - } + use LazilyRefreshDatabase; + use WithFaker; + use WithWorkbench; protected function afterRefreshingDatabase() { $this->setUpFaker(); - UserFactory::new()->count(50)->state(new Sequence(function () { + SearchableUserFactory::new()->count(50)->state(new Sequence(function () { return ['name' => 'Laravel '.$this->faker()->name()]; }))->create(); - UserFactory::new()->times(50)->create(); + SearchableUserFactory::new()->times(50)->create(); } public function test_it_can_paginate_without_custom_query_callback() { - $this->prepareScoutSearchMockUsing('Laravel'); - - $paginator = SearchableUserModel::search('Laravel')->paginate(); + $paginator = SearchableUser::search('Laravel')->paginate(); $this->assertSame(50, $paginator->total()); $this->assertSame(4, $paginator->lastPage()); @@ -48,9 +42,7 @@ public function test_it_can_paginate_without_custom_query_callback() public function test_it_can_paginate_with_custom_query_callback() { - $this->prepareScoutSearchMockUsing('Laravel'); - - $paginator = SearchableUserModel::search('Laravel')->query(function ($builder) { + $paginator = SearchableUser::search('Laravel')->query(function ($builder) { return $builder->where('id', '<', 11); })->paginate(); @@ -61,60 +53,10 @@ public function test_it_can_paginate_with_custom_query_callback() public function test_it_can_paginate_raw_without_custom_query_callback() { - $this->prepareScoutSearchMockUsing('Laravel'); - - $paginator = SearchableUserModel::search('Laravel')->paginateRaw(); + $paginator = SearchableUser::search('Laravel')->paginateRaw(); $this->assertSame(50, $paginator->total()); $this->assertSame(4, $paginator->lastPage()); $this->assertSame(15, $paginator->perPage()); } - - public function test_it_can_paginate_raw_with_custom_query_callback() - { - $this->prepareScoutSearchMockUsing('Laravel'); - - $paginator = SearchableUserModel::search('Laravel')->query(function ($builder) { - return $builder->where('id', '<', 11); - })->paginateRaw(); - - $this->assertSame(10, $paginator->total()); - $this->assertSame(1, $paginator->lastPage()); - $this->assertSame(15, $paginator->perPage()); - } - - protected function prepareScoutSearchMockUsing($searchQuery) - { - $engine = m::mock('Meilisearch\Client'); - $indexes = m::mock('Meilisearch\Endpoints\Indexes'); - - $manager = $this->app->make(EngineManager::class); - $manager->extend('fake', function () use ($engine) { - return new MeilisearchEngine($engine); - }); - - $query = User::where('name', 'like', $searchQuery.'%'); - - $hitsPerPage = 15; - $page = 1; - $totalPages = intval($query->count() / $hitsPerPage); - - $engine->shouldReceive('index')->with('users')->andReturn($indexes); - $indexes->shouldReceive('rawSearch') - ->with($searchQuery, ['hitsPerPage' => $hitsPerPage, 'page' => $page]) - ->andReturn([ - 'query' => $searchQuery, - 'hits' => $query->get()->transform(function ($result) { - return [ - 'id' => $result->getKey(), - 'name' => $result->name, - ]; - })->toArray(), - 'hitsPerPage' => $hitsPerPage, - 'page' => $page, - 'totalHits' => $query->count(), - 'totalPages' => $totalPages > 0 ? $totalPages : 0, - 'processingTimeMs' => 1, - ]); - } } diff --git a/tests/Feature/CollectionEngineTest.php b/tests/Feature/CollectionEngineTest.php index 2f07a5c9..f8570a4a 100644 --- a/tests/Feature/CollectionEngineTest.php +++ b/tests/Feature/CollectionEngineTest.php @@ -4,23 +4,20 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\LazilyRefreshDatabase; -use Laravel\Scout\Tests\Fixtures\SearchableModelWithUnloadedValue; -use Laravel\Scout\Tests\Fixtures\SearchableUserModel; -use Laravel\Scout\Tests\Fixtures\SearchableUserModelWithCustomCreatedAt; -use Laravel\Scout\Tests\Fixtures\SearchableUserModelWithCustomSearchableData; -use Orchestra\Testbench\Concerns\WithLaravelMigrations; +use Illuminate\Support\Collection; +use Orchestra\Testbench\Attributes\WithConfig; +use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\Factories\UserFactory; use Orchestra\Testbench\TestCase; +use Workbench\App\Models\SearchableUser; +#[WithConfig('scout.driver', 'collection')] +#[WithMigration] class CollectionEngineTest extends TestCase { - use LazilyRefreshDatabase, WithLaravelMigrations, WithWorkbench; - - protected function defineEnvironment($app) - { - $app->make('config')->set('scout.driver', 'collection'); - } + use LazilyRefreshDatabase; + use WithWorkbench; protected function afterRefreshingDatabase() { @@ -39,113 +36,113 @@ protected function afterRefreshingDatabase() public function test_it_can_retrieve_results_with_empty_search() { - $models = SearchableUserModel::search()->get(); + $models = SearchableUser::search()->get(); $this->assertCount(2, $models); } public function test_it_can_retrieve_results() { - $models = SearchableUserModel::search('Taylor')->where('email', 'taylor@laravel.com')->get(); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->get(); $this->assertCount(1, $models); $this->assertEquals(1, $models[0]->id); - $models = SearchableUserModel::search('Taylor')->query(function ($query) { + $models = SearchableUser::search('Taylor')->query(function ($query) { $query->where('email', 'like', 'taylor@laravel.com'); })->get(); $this->assertCount(1, $models); $this->assertEquals(1, $models[0]->id); - $models = SearchableUserModel::search('Abigail')->where('email', 'abigail@laravel.com')->get(); + $models = SearchableUser::search('Abigail')->where('email', 'abigail@laravel.com')->get(); $this->assertCount(1, $models); $this->assertEquals(2, $models[0]->id); - $models = SearchableUserModel::search('Taylor')->where('email', 'abigail@laravel.com')->get(); + $models = SearchableUser::search('Taylor')->where('email', 'abigail@laravel.com')->get(); $this->assertCount(0, $models); - $models = SearchableUserModel::search('Taylor')->where('email', 'taylor@laravel.com')->get(); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->get(); $this->assertCount(1, $models); - $models = SearchableUserModel::search('otwell')->get(); + $models = SearchableUser::search('otwell')->get(); $this->assertCount(2, $models); - $models = SearchableUserModel::search('laravel')->get(); + $models = SearchableUser::search('laravel')->get(); $this->assertCount(2, $models); - $models = SearchableUserModel::search('foo')->get(); + $models = SearchableUser::search('foo')->get(); $this->assertCount(0, $models); - $models = SearchableUserModel::search('Abigail')->where('email', 'taylor@laravel.com')->get(); + $models = SearchableUser::search('Abigail')->where('email', 'taylor@laravel.com')->get(); $this->assertCount(0, $models); } public function test_it_can_retrieve_results_matching_to_custom_searchable_data() { - $models = SearchableUserModelWithCustomSearchableData::search('rolyaT')->get(); + $models = SearchableUserWithCustomSearchableData::search('rolyaT')->get(); $this->assertCount(1, $models); } public function test_it_can_paginate_results() { - $models = SearchableUserModel::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(); $this->assertCount(1, $models); - $models = SearchableUserModel::search('Taylor')->where('email', 'abigail@laravel.com')->paginate(); + $models = SearchableUser::search('Taylor')->where('email', 'abigail@laravel.com')->paginate(); $this->assertCount(0, $models); - $models = SearchableUserModel::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(); $this->assertCount(1, $models); - $models = SearchableUserModel::search('laravel')->paginate(); + $models = SearchableUser::search('laravel')->paginate(); $this->assertCount(2, $models); $dummyQuery = function ($query) { $query->where('name', '!=', 'Dummy'); }; - $models = SearchableUserModel::search('laravel')->query($dummyQuery)->orderBy('name')->paginate(1, 'page', 1); + $models = SearchableUser::search('laravel')->query($dummyQuery)->orderBy('name')->paginate(1, 'page', 1); $this->assertCount(1, $models); $this->assertEquals('Abigail Otwell', $models[0]->name); - $models = SearchableUserModel::search('laravel')->query($dummyQuery)->orderBy('name')->paginate(1, 'page', 2); + $models = SearchableUser::search('laravel')->query($dummyQuery)->orderBy('name')->paginate(1, 'page', 2); $this->assertCount(1, $models); $this->assertEquals('Taylor Otwell', $models[0]->name); } public function test_limit_is_applied() { - $models = SearchableUserModel::search('laravel')->get(); + $models = SearchableUser::search('laravel')->get(); $this->assertCount(2, $models); - $models = SearchableUserModel::search('laravel')->take(1)->get(); + $models = SearchableUser::search('laravel')->take(1)->get(); $this->assertCount(1, $models); } public function test_it_can_order_results() { - $models = SearchableUserModel::search('laravel')->orderBy('name', 'asc')->paginate(1, 'page', 1); + $models = SearchableUser::search('laravel')->orderBy('name', 'asc')->paginate(1, 'page', 1); $this->assertCount(1, $models); $this->assertEquals('Abigail Otwell', $models[0]->name); - $models = SearchableUserModel::search('laravel')->orderBy('name', 'desc')->paginate(1, 'page', 1); + $models = SearchableUser::search('laravel')->orderBy('name', 'desc')->paginate(1, 'page', 1); $this->assertCount(1, $models); $this->assertEquals('Taylor Otwell', $models[0]->name); } public function test_it_can_order_by_latest_and_oldest() { - $models = SearchableUserModel::search('laravel')->latest()->paginate(1, 'page', 1); + $models = SearchableUser::search('laravel')->latest()->paginate(1, 'page', 1); $this->assertCount(1, $models); $this->assertEquals('Abigail Otwell', $models[0]->name); - $models = SearchableUserModel::search('laravel')->oldest()->paginate(1, 'page', 1); + $models = SearchableUser::search('laravel')->oldest()->paginate(1, 'page', 1); $this->assertCount(1, $models); $this->assertEquals('Taylor Otwell', $models[0]->name); } public function test_it_can_order_by_custom_model_created_at_timestamp() { - $query = SearchableUserModelWithCustomCreatedAt::search()->latest(); + $query = SearchableUserWithCustomCreatedAt::search()->latest(); $this->assertCount(1, $query->orders); $this->assertEquals('created', $query->orders[0]['column']); @@ -155,8 +152,43 @@ public function test_it_calls_make_searchable_using_before_searching() { Model::preventAccessingMissingAttributes(true); - $models = SearchableModelWithUnloadedValue::search('loaded')->get(); + $models = SearchableUserWithUnloadedValue::search('loaded')->get(); $this->assertCount(2, $models); } } + +class SearchableUserWithCustomCreatedAt extends SearchableUser +{ + public const CREATED_AT = 'created'; +} + +class SearchableUserWithCustomSearchableData extends SearchableUser +{ + /** {@inheritDoc} */ + public function toSearchableArray(): array + { + return [ + 'reversed_name' => strrev($this->name), + ]; + } +} + +class SearchableUserWithUnloadedValue extends SearchableUser +{ + /** {@inheritDoc} */ + public function toSearchableArray() + { + return [ + 'value' => $this->unloadedValue, + ]; + } + + /** {@inheritDoc} */ + public function makeSearchableUsing(Collection $models) + { + return $models->each( + fn ($model) => $model->unloadedValue = 'loaded', + ); + } +} diff --git a/tests/Feature/DatabaseEngineTest.php b/tests/Feature/DatabaseEngineTest.php index e3f6df12..848b6485 100644 --- a/tests/Feature/DatabaseEngineTest.php +++ b/tests/Feature/DatabaseEngineTest.php @@ -4,11 +4,11 @@ use Illuminate\Foundation\Testing\LazilyRefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; -use Laravel\Scout\Tests\Fixtures\SearchableUserDatabaseModel; use Orchestra\Testbench\Concerns\WithLaravelMigrations; use Orchestra\Testbench\Concerns\WithWorkbench; -use Orchestra\Testbench\Factories\UserFactory; use Orchestra\Testbench\TestCase; +use Workbench\App\Models\SearchableUser; +use Workbench\Database\Factories\SearchableUserFactory; class DatabaseEngineTest extends TestCase { @@ -21,12 +21,12 @@ protected function defineEnvironment($app) protected function afterRefreshingDatabase() { - UserFactory::new()->create([ + SearchableUserFactory::new()->create([ 'name' => 'Taylor Otwell', 'email' => 'taylor@laravel.com', ]); - UserFactory::new()->create([ + SearchableUserFactory::new()->create([ 'name' => 'Abigail Otwell', 'email' => 'abigail@laravel.com', ]); @@ -34,115 +34,115 @@ protected function afterRefreshingDatabase() public function test_it_can_retrieve_results_with_empty_search() { - $models = SearchableUserDatabaseModel::search()->get(); + $models = SearchableUser::search()->get(); $this->assertCount(2, $models); } public function test_it_does_not_add_search_where_clauses_with_empty_search() { - SearchableUserDatabaseModel::search('')->query(function ($builder) { + SearchableUser::search('')->query(function ($builder) { $this->assertSame('select * from "users"', $builder->toSql()); })->get(); } public function test_it_adds_search_where_clauses_with_non_empty_search() { - SearchableUserDatabaseModel::search('Taylor')->query(function ($builder) { + SearchableUser::search('Taylor')->query(function ($builder) { $this->assertSame('select * from "users" where ("users"."id" like ? or "users"."name" like ? or "users"."email" like ?)', $builder->toSql()); })->get(); } public function test_it_can_retrieve_results() { - $models = SearchableUserDatabaseModel::search('Taylor')->where('email', 'taylor@laravel.com')->get(); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->get(); $this->assertCount(1, $models); $this->assertEquals(1, $models[0]->id); - $models = SearchableUserDatabaseModel::search('Taylor')->query(function ($query) { + $models = SearchableUser::search('Taylor')->query(function ($query) { $query->where('email', 'like', 'taylor@laravel.com'); })->get(); $this->assertCount(1, $models); $this->assertEquals(1, $models[0]->id); - $models = SearchableUserDatabaseModel::search('Abigail')->where('email', 'abigail@laravel.com')->get(); + $models = SearchableUser::search('Abigail')->where('email', 'abigail@laravel.com')->get(); $this->assertCount(1, $models); $this->assertEquals(2, $models[0]->id); - $models = SearchableUserDatabaseModel::search('Taylor')->where('email', 'abigail@laravel.com')->get(); + $models = SearchableUser::search('Taylor')->where('email', 'abigail@laravel.com')->get(); $this->assertCount(0, $models); - $models = SearchableUserDatabaseModel::search('Taylor')->where('email', 'taylor@laravel.com')->get(); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->get(); $this->assertCount(1, $models); - $models = SearchableUserDatabaseModel::search('otwell')->get(); + $models = SearchableUser::search('otwell')->get(); $this->assertCount(2, $models); - $models = SearchableUserDatabaseModel::search('laravel')->get(); + $models = SearchableUser::search('laravel')->get(); $this->assertCount(2, $models); - $models = SearchableUserDatabaseModel::search('foo')->get(); + $models = SearchableUser::search('foo')->get(); $this->assertCount(0, $models); - $models = SearchableUserDatabaseModel::search('Abigail')->where('email', 'taylor@laravel.com')->get(); + $models = SearchableUser::search('Abigail')->where('email', 'taylor@laravel.com')->get(); $this->assertCount(0, $models); } public function test_it_can_paginate_results() { - $models = SearchableUserDatabaseModel::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(); $this->assertCount(1, $models); - $models = SearchableUserDatabaseModel::search('Taylor')->where('email', 'abigail@laravel.com')->paginate(); + $models = SearchableUser::search('Taylor')->where('email', 'abigail@laravel.com')->paginate(); $this->assertCount(0, $models); - $models = SearchableUserDatabaseModel::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(); $this->assertCount(1, $models); - $models = SearchableUserDatabaseModel::search('laravel')->paginate(); + $models = SearchableUser::search('laravel')->paginate(); $this->assertCount(2, $models); } public function test_it_can_paginate_using_a_custom_page_name() { - $models = SearchableUserDatabaseModel::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(); $this->assertStringContainsString('page=1', $models->url(1)); - $models = SearchableUserDatabaseModel::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(pageName: 'foo'); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(pageName: 'foo'); $this->assertStringContainsString('foo=1', $models->url(1)); - $models = SearchableUserDatabaseModel::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(pageName: 'bar'); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->paginate(pageName: 'bar'); $this->assertStringContainsString('bar=1', $models->url(1)); } public function test_it_can_simple_paginate_using_a_custom_page_name() { - $models = SearchableUserDatabaseModel::search('Taylor')->where('email', 'taylor@laravel.com')->simplePaginate(); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->simplePaginate(); $this->assertStringContainsString('page=1', $models->url(1)); - $models = SearchableUserDatabaseModel::search('Taylor')->where('email', 'taylor@laravel.com')->simplePaginate(pageName: 'foo'); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->simplePaginate(pageName: 'foo'); $this->assertStringContainsString('foo=1', $models->url(1)); - $models = SearchableUserDatabaseModel::search('Taylor')->where('email', 'taylor@laravel.com')->simplePaginate(pageName: 'bar'); + $models = SearchableUser::search('Taylor')->where('email', 'taylor@laravel.com')->simplePaginate(pageName: 'bar'); $this->assertStringContainsString('bar=1', $models->url(1)); } public function test_limit_is_applied() { - $models = SearchableUserDatabaseModel::search('laravel')->get(); + $models = SearchableUser::search('laravel')->get(); $this->assertCount(2, $models); - $models = SearchableUserDatabaseModel::search('laravel')->take(1)->get(); + $models = SearchableUser::search('laravel')->take(1)->get(); $this->assertCount(1, $models); } public function test_tap_is_applied() { - $models = SearchableUserDatabaseModel::search('laravel')->get(); + $models = SearchableUser::search('laravel')->get(); $this->assertCount(2, $models); - $models = SearchableUserDatabaseModel::search('laravel')->tap(function ($query) { + $models = SearchableUser::search('laravel')->tap(function ($query) { return $query->take(1); })->get(); $this->assertCount(1, $models); @@ -150,27 +150,27 @@ public function test_tap_is_applied() public function test_it_can_order_results() { - $models = SearchableUserDatabaseModel::search('laravel')->orderBy('name', 'asc')->take(1)->get(); + $models = SearchableUser::search('laravel')->orderBy('name', 'asc')->take(1)->get(); $this->assertCount(1, $models); $this->assertEquals('Abigail Otwell', $models[0]->name); - $modelsPaginate = SearchableUserDatabaseModel::search('laravel')->orderBy('name', 'asc')->paginate(1, 'page', 1); + $modelsPaginate = SearchableUser::search('laravel')->orderBy('name', 'asc')->paginate(1, 'page', 1); $this->assertCount(1, $modelsPaginate); $this->assertEquals('Abigail Otwell', $modelsPaginate[0]->name); - $modelsSimplePaginate = SearchableUserDatabaseModel::search('laravel')->orderBy('name', 'asc')->simplePaginate(1, 'page', 1); + $modelsSimplePaginate = SearchableUser::search('laravel')->orderBy('name', 'asc')->simplePaginate(1, 'page', 1); $this->assertCount(1, $modelsPaginate); $this->assertEquals('Abigail Otwell', $modelsSimplePaginate[0]->name); - $models = SearchableUserDatabaseModel::search('laravel')->orderBy('name', 'desc')->take(1)->get(); + $models = SearchableUser::search('laravel')->orderBy('name', 'desc')->take(1)->get(); $this->assertCount(1, $models); $this->assertEquals('Taylor Otwell', $models[0]->name); - $modelsPaginate = SearchableUserDatabaseModel::search('laravel')->orderBy('name', 'desc')->paginate(1, 'page', 1); + $modelsPaginate = SearchableUser::search('laravel')->orderBy('name', 'desc')->paginate(1, 'page', 1); $this->assertCount(1, $modelsPaginate); $this->assertEquals('Taylor Otwell', $modelsPaginate[0]->name); - $modelsSimplePaginate = SearchableUserDatabaseModel::search('laravel')->orderBy('name', 'desc')->simplePaginate(1, 'page', 1); + $modelsSimplePaginate = SearchableUser::search('laravel')->orderBy('name', 'desc')->simplePaginate(1, 'page', 1); $this->assertCount(1, $modelsSimplePaginate); $this->assertEquals('Taylor Otwell', $modelsSimplePaginate[0]->name); } diff --git a/tests/Feature/Engines/Algolia3EngineTest.php b/tests/Feature/Engines/Algolia3EngineTest.php new file mode 100644 index 00000000..cd46d65e --- /dev/null +++ b/tests/Feature/Engines/Algolia3EngineTest.php @@ -0,0 +1,246 @@ +client = m::spy(SearchClient::class); + + $manager->extend('algolia3-testing', fn () => new Algolia3Engine($this->client, config('scout.soft_delete'))); + }); + + $this->beforeApplicationDestroyed(function () { + unset($this->client); + }); + } + + public function test_update_adds_objects_to_index() + { + $model = SearchableUserFactory::new()->createQuietly(); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('initIndex')->once()->with('users')->andReturn($index = m::mock(stdClass::class)); + $index->shouldReceive('saveObjects')->once()->with([[ + 'id' => $model->getKey(), + 'name' => $model->name, + 'email' => $model->email, + 'objectID' => $model->getScoutKey(), + ]]); + + $engine->update(Collection::make([$model])); + } + + public function test_delete_removes_objects_to_index() + { + $model = SearchableUserFactory::new()->createQuietly(); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('initIndex')->once()->with('users')->andReturn($index = m::mock(stdClass::class)); + $index->shouldReceive('deleteObjects')->once()->with([1]); + + $engine->delete(Collection::make([$model])); + } + + public function test_delete_removes_objects_to_index_with_a_custom_search_key() + { + $model = ChirpFactory::new()->createQuietly([ + 'scout_id' => 'my-algolia-key.5', + ]); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('initIndex')->once()->with('chirps')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('deleteObjects')->once()->with(['my-algolia-key.5']); + + $engine->delete(Collection::make([$model])); + } + + public function test_delete_with_removeable_scout_collection_using_custom_search_key() + { + $model = ChirpFactory::new()->createQuietly([ + 'scout_id' => 'my-algolia-key.5', + ]); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $job = new RemoveFromSearch(RemoveableScoutCollection::make([$model])); + + $job = unserialize(serialize($job)); + + $this->client->shouldReceive('initIndex')->once()->with('chirps')->andReturn($index = m::mock(stdClass::class)); + $index->shouldReceive('deleteObjects')->once()->with(['my-algolia-key.5']); + + $job->handle(); + } + + public function test_search_sends_correct_parameters_to_algolia() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('initIndex')->once()->with('users')->andReturn($index = m::mock(stdClass::class)); + $index->shouldReceive('search')->once()->with('zonda', [ + 'numericFilters' => ['foo=1'], + ])->once(); + + $builder = new Builder(new SearchableUser, 'zonda'); + $builder->where('foo', 1); + + $engine->search($builder); + } + + public function test_search_sends_correct_parameters_to_algolia_for_where_in_search() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('initIndex')->once()->with('users')->andReturn($index = m::mock(stdClass::class)); + $index->shouldReceive('search')->once()->with('zonda', [ + 'numericFilters' => ['foo=1', ['bar=1', 'bar=2']], + ]); + + $builder = new Builder(new SearchableUser, 'zonda'); + $builder->where('foo', 1)->whereIn('bar', [1, 2]); + + $engine->search($builder); + } + + public function test_search_sends_correct_parameters_to_algolia_for_empty_where_in_search() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('initIndex')->once()->with('users')->andReturn($index = m::mock(stdClass::class)); + $index->shouldReceive('search')->once()->with('zonda', [ + 'numericFilters' => ['foo=1', '0=1'], + ]); + + $builder = new Builder(new SearchableUser, 'zonda'); + $builder->where('foo', 1)->whereIn('bar', []); + $engine->search($builder); + } + + public function test_map_correctly_maps_results_to_models() + { + $model = SearchableUserFactory::new()->createQuietly(['name' => 'zonda']); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $builder = m::mock(Builder::class); + + $results = $engine->map($builder, [ + 'nbHits' => 1, + 'hits' => [ + ['objectID' => 1, 'id' => 1, '_rankingInfo' => ['nbTypos' => 0]], + ], + ], $model); + + $this->assertCount(1, $results); + $this->assertEquals(['_rankingInfo' => ['nbTypos' => 0]], $results->first()->scoutMetaData()); + Assert::assertArraySubset(['id' => 1, 'name' => 'zonda'], $results->first()->toArray()); + } + + public function test_a_model_is_indexed_with_a_custom_algolia_key() + { + $model = ChirpFactory::new()->createQuietly([ + 'scout_id' => 'my-algolia-key.1', + ]); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('initIndex')->once()->with('chirps')->andReturn($index = m::mock(stdClass::class)); + $index->shouldReceive('saveObjects')->once()->with([[ + 'content' => $model->content, + 'objectID' => 'my-algolia-key.1', + ]]); + + $engine->update(Collection::make([$model])); + } + + public function test_a_model_is_removed_with_a_custom_algolia_key() + { + $model = ChirpFactory::new()->createQuietly([ + 'scout_id' => 'my-algolia-key.1', + ]); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('initIndex')->once()->with('chirps')->andReturn($index = m::mock(stdClass::class)); + $index->shouldReceive('deleteObjects')->once()->with(['my-algolia-key.1']); + + $engine->delete(Collection::make([$model])); + } + + public function test_flush_a_model_with_a_custom_algolia_key() + { + $model = ChirpFactory::new()->createQuietly([ + 'scout_id' => 'my-algolia-key.1', + ]); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('initIndex')->once()->with('chirps')->andReturn($index = m::mock(stdClass::class)); + $index->shouldReceive('clearObjects')->once(); + + $engine->flush(new Chirp); + } + + public function test_update_empty_searchable_array_does_not_add_objects_to_index() + { + $_ENV['user.toSearchableArray'] = []; + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('initIndex')->once()->with('users')->andReturn($index = m::mock(stdClass::class)); + $index->shouldNotReceive('saveObjects'); + + $engine->update(Collection::make([new SearchableUser])); + + unset($_ENV['user.toSearchableArray']); + } + + #[WithConfig('scout.soft_delete', true)] + public function test_update_empty_searchable_array_from_soft_deleted_model_does_not_add_objects_to_index() + { + $_ENV['chirp.toSearchableArray'] = []; + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('initIndex')->once()->with('chirps')->andReturn($index = m::mock(stdClass::class)); + $index->shouldNotReceive('saveObjects'); + + $engine->update(Collection::make([new Chirp])); + + unset($_ENV['chirp.toSearchableArray']); + } +} diff --git a/tests/Feature/Engines/Algolia4EngineTest.php b/tests/Feature/Engines/Algolia4EngineTest.php new file mode 100644 index 00000000..3c02de3c --- /dev/null +++ b/tests/Feature/Engines/Algolia4EngineTest.php @@ -0,0 +1,240 @@ +client = m::spy(SearchClient::class); + + $manager->extend('algolia4-testing', fn () => new Algolia4Engine($this->client, config('scout.soft_delete'))); + }); + + $this->beforeApplicationDestroyed(function () { + unset($this->client); + }); + } + + public function test_update_adds_objects_to_index() + { + $model = SearchableUserFactory::new()->createQuietly(); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('saveObjects')->once()->with('users', [[ + 'id' => $model->getKey(), + 'name' => $model->name, + 'email' => $model->email, + 'objectID' => $model->getScoutKey(), + ]]); + + $engine->update(Collection::make([$model])); + } + + public function test_delete_removes_objects_to_index() + { + $model = SearchableUserFactory::new()->createQuietly(); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('deleteObjects')->once()->with('users', [1]); + + $engine->delete(Collection::make([$model])); + } + + public function test_delete_removes_objects_to_index_with_a_custom_search_key() + { + $model = ChirpFactory::new()->createQuietly([ + 'scout_id' => 'my-algolia-key.5', + ]); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('deleteObjects')->once()->with('chirps', ['my-algolia-key.5']); + + $engine->delete(Collection::make([$model])); + } + + public function test_delete_with_removeable_scout_collection_using_custom_search_key() + { + $model = ChirpFactory::new()->createQuietly([ + 'scout_id' => 'my-algolia-key.5', + ]); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $job = new RemoveFromSearch(RemoveableScoutCollection::make([$model])); + + $job = unserialize(serialize($job)); + + $this->client->shouldReceive('deleteObjects')->once()->with('chirps', ['my-algolia-key.5']); + + $job->handle(); + } + + public function test_search_sends_correct_parameters_to_algolia() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('searchSingleIndex')->once()->with( + 'users', + ['query' => 'zonda'], + ['numericFilters' => ['foo=1']] + ); + + $builder = new Builder(new SearchableUser, 'zonda'); + $builder->where('foo', 1); + + $engine->search($builder); + } + + public function test_search_sends_correct_parameters_to_algolia_for_where_in_search() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('searchSingleIndex')->once()->with( + 'users', + ['query' => 'zonda'], + ['numericFilters' => ['foo=1', ['bar=1', 'bar=2']]], + ); + + $builder = new Builder(new SearchableUser, 'zonda'); + $builder->where('foo', 1)->whereIn('bar', [1, 2]); + + $engine->search($builder); + } + + public function test_search_sends_correct_parameters_to_algolia_for_empty_where_in_search() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('searchSingleIndex')->once()->with( + 'users', + ['query' => 'zonda'], + ['numericFilters' => ['foo=1', '0=1']] + ); + + $builder = new Builder(new SearchableUser, 'zonda'); + $builder->where('foo', 1)->whereIn('bar', []); + $engine->search($builder); + } + + public function test_map_correctly_maps_results_to_models() + { + $model = SearchableUserFactory::new()->createQuietly(['name' => 'zonda']); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $builder = m::mock(Builder::class); + + $results = $engine->map($builder, [ + 'nbHits' => 1, + 'hits' => [ + ['objectID' => 1, 'id' => 1, '_rankingInfo' => ['nbTypos' => 0]], + ], + ], $model); + + $this->assertCount(1, $results); + $this->assertEquals(['_rankingInfo' => ['nbTypos' => 0]], $results->first()->scoutMetaData()); + Assert::assertArraySubset(['id' => 1, 'name' => 'zonda'], $results->first()->toArray()); + } + + public function test_a_model_is_indexed_with_a_custom_algolia_key() + { + $model = ChirpFactory::new()->createQuietly([ + 'scout_id' => 'my-algolia-key.1', + ]); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('saveObjects')->once()->with('chirps', [[ + 'content' => $model->content, + 'objectID' => 'my-algolia-key.1', + ]]); + + $engine->update(Collection::make([$model])); + } + + public function test_a_model_is_removed_with_a_custom_algolia_key() + { + $model = ChirpFactory::new()->createQuietly([ + 'scout_id' => 'my-algolia-key.1', + ]); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('deleteObjects')->once()->with('chirps', ['my-algolia-key.1']); + + $engine->delete(Collection::make([$model])); + } + + public function test_flush_a_model_with_a_custom_algolia_key() + { + $model = ChirpFactory::new()->createQuietly([ + 'scout_id' => 'my-algolia-key.1', + ]); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('clearObjects')->once()->with('chirps'); + + $engine->flush(new Chirp); + } + + public function test_update_empty_searchable_array_does_not_add_objects_to_index() + { + $_ENV['searchable.user'] = []; + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldNotReceive('saveObjects')->with('users'); + + $engine->update(Collection::make([new SearchableUser])); + + unset($_ENV['searchable.user']); + } + + #[WithConfig('scout.soft_delete', true)] + public function test_update_empty_searchable_array_from_soft_deleted_model_does_not_add_objects_to_index() + { + $_ENV['searchable.chirp'] = []; + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldNotReceive('saveObjects')->with('chirps'); + + $engine->update(Collection::make([new Chirp])); + + unset($_ENV['searchable.chirp']); + } +} diff --git a/tests/Feature/Engines/MeilisearchEngineTest.php b/tests/Feature/Engines/MeilisearchEngineTest.php new file mode 100644 index 00000000..8c8053bf --- /dev/null +++ b/tests/Feature/Engines/MeilisearchEngineTest.php @@ -0,0 +1,435 @@ +client = m::spy(SearchClient::class); + + $manager->extend('meilisearch-testing', fn () => new MeilisearchEngine($this->client, config('scout.soft_delete'))); + }); + + $this->beforeApplicationDestroyed(function () { + unset($this->client); + }); + } + + public function test_update_adds_objects_to_index() + { + $model = SearchableUserFactory::new()->createQuietly(); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('addDocuments')->once()->with( + [$model->toSearchableArray()], 'id' + ); + + $engine->update(Collection::make([$model])); + } + + public function test_delete_removes_objects_to_index() + { + $model = SearchableUserFactory::new()->createQuietly(); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('deleteDocuments')->once()->with([1]); + + $engine->delete(Collection::make([$model])); + } + + public function test_delete_removes_objects_to_index_with_a_custom_search_key() + { + $model = ChirpFactory::new()->createQuietly(['scout_id' => 'my-meilisearch-key.5']); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('index')->once()->with('chirps')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('deleteDocuments')->once()->with(['my-meilisearch-key.5']); + + $engine->delete(Collection::make([$model])); + } + + public function test_delete_with_removeable_scout_collection_using_custom_search_key() + { + $model = ChirpFactory::new()->createQuietly(['scout_id' => 'my-meilisearch-key.5']); + + $job = new RemoveFromSearch(RemoveableScoutCollection::make([$model])); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $job = unserialize(serialize($job)); + + $this->client->shouldReceive('index')->once()->with('chirps')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('deleteDocuments')->once()->with(['my-meilisearch-key.5']); + + $job->handle(); + } + + public function test_search_sends_correct_parameters_to_meilisearch() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('search')->once()->with('mustang', [ + 'filter' => 'foo=1 AND bar=2', + ]); + + $builder = new Builder(new SearchableUser, 'mustang', function ($meilisearch, $query, $options) { + $options['filter'] = 'foo=1 AND bar=2'; + + return $meilisearch->search($query, $options); + }); + + $engine->search($builder); + } + + public function test_search_includes_at_least_scoutKeyName_in_attributesToRetrieve_on_builder_options() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('search')->once()->with('mustang', [ + 'filter' => 'foo=1 AND bar=2', + 'attributesToRetrieve' => ['id', 'foo'], + ]); + + $builder = new Builder(new SearchableUser, 'mustang', function ($meilisearch, $query, $options) { + $options['filter'] = 'foo=1 AND bar=2'; + + return $meilisearch->search($query, $options); + }); + $builder->options = ['attributesToRetrieve' => ['foo']]; + + $engine->search($builder); + } + + public function test_submitting_a_callable_search_with_search_method_returns_array() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $builder = new Builder( + new SearchableUser, + $query = 'mustang', + $callable = function ($meilisearch, $query, $options) { + $options['filter'] = 'foo=1'; + + return $meilisearch->search($query, $options); + } + ); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('search')->once()->with($query, ['filter' => 'foo=1'])->andReturn(new SearchResult($expectedResult = [ + 'hits' => [], + 'page' => 1, + 'hitsPerPage' => $builder->limit, + 'totalPages' => 1, + 'totalHits' => 0, + 'processingTimeMs' => 1, + 'query' => 'mustang', + ])); + + $result = $engine->search($builder); + + $this->assertSame($expectedResult, $result); + } + + public function test_submitting_a_callable_search_with_raw_search_method_works() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $builder = new Builder( + new SearchableUser, + $query = 'mustang', + $callable = function ($meilisearch, $query, $options) { + $options['filter'] = 'foo=1'; + + return $meilisearch->rawSearch($query, $options); + } + ); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('rawSearch')->once()->with($query, ['filter' => 'foo=1'])->andReturn($expectedResult = [ + 'hits' => [], + 'page' => 1, + 'hitsPerPage' => $builder->limit, + 'totalPages' => 1, + 'totalHits' => 0, + 'processingTimeMs' => 1, + 'query' => $query, + ]); + + $result = $engine->search($builder); + + $this->assertSame($expectedResult, $result); + } + + public function test_where_in_conditions_are_applied() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $builder = new Builder(new SearchableUser, ''); + $builder->where('foo', 'bar'); + $builder->where('bar', 'baz'); + $builder->whereIn('qux', [1, 2]); + $builder->whereIn('quux', [1, 2]); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ + 'filter' => 'foo="bar" AND bar="baz" AND qux IN [1, 2] AND quux IN [1, 2]', + 'hitsPerPage' => $builder->limit, + ]))->andReturn([]); + + $engine->search($builder); + } + + public function test_a_model_is_indexed_with_a_custom_meilisearch_key() + { + $model = ChirpFactory::new()->createQuietly(['scout_id' => 'my-meilisearch-key.5']); + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('index')->once()->with('chirps')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('addDocuments')->once()->with([[ + 'scout_id' => 'my-meilisearch-key.5', + 'content' => $model->content, + ]], 'scout_id'); + + $engine->update(Collection::make([$model])); + } + + public function test_flush_a_model_with_a_custom_meilisearch_key() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('index')->once()->with('chirps')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('deleteAllDocuments'); + + $engine->flush(new Chirp); + } + + public function test_update_empty_searchable_array_does_not_add_documents_to_index() + { + $_ENV['user.toSearchableArray'] = []; + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldNotReceive('addDocuments'); + + $engine->update(Collection::make([new SearchableUser])); + + unset($_ENV['user.toSearchableArray']); + } + + public function test_pagination_correct_parameters() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $perPage = 5; + $page = 2; + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('search')->once()->with('mustang', [ + 'filter' => 'foo=1', + 'hitsPerPage' => $perPage, + 'page' => $page, + ]); + + $builder = new Builder(new SearchableUser, 'mustang', function ($meilisearch, $query, $options) { + $options['filter'] = 'foo=1'; + + return $meilisearch->search($query, $options); + }); + + $engine->paginate($builder, $perPage, $page); + } + + public function test_pagination_sorted_parameter() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $perPage = 5; + $page = 2; + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('search')->once()->with('mustang', [ + 'filter' => 'foo=1', + 'hitsPerPage' => $perPage, + 'page' => $page, + 'sort' => ['name:asc'], + ]); + + $builder = new Builder(new SearchableUser, 'mustang', function ($meilisearch, $query, $options) { + $options['filter'] = 'foo=1'; + + return $meilisearch->search($query, $options); + }); + $builder->orderBy('name', 'asc'); + + $engine->paginate($builder, $perPage, $page); + } + + #[WithConfig('scout.soft_delete', true)] + public function test_update_empty_searchable_array_from_soft_deleted_model_does_not_add_documents_to_index() + { + $_ENV['chirp.toSearchableArray'] = []; + + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('index')->once()->with('chirps')->andReturn($index = m::mock(Indexes::class)); + $index->shouldNotReceive('addDocuments'); + + $engine->update(Collection::make([new Chirp])); + + unset($_ENV['chirp.toSearchableArray']); + } + + public function test_performing_search_without_callback_works() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $builder = new Builder(new SearchableUser, ''); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('rawSearch')->once()->andReturn([]); + + $engine->search($builder); + } + + public function test_where_conditions_are_applied() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $builder = new Builder(new SearchableUser, ''); + $builder->where('foo', 'bar'); + $builder->where('key', 'value'); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ + 'filter' => 'foo="bar" AND key="value"', + 'hitsPerPage' => $builder->limit, + ]))->andReturn([]); + + $engine->search($builder); + } + + public function test_where_not_in_conditions_are_applied() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $builder = new Builder(new SearchableUser, ''); + $builder->where('foo', 'bar'); + $builder->where('bar', 'baz'); + $builder->whereIn('qux', [1, 2]); + $builder->whereIn('quux', [1, 2]); + $builder->whereNotIn('eaea', [3]); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ + 'filter' => 'foo="bar" AND bar="baz" AND qux IN [1, 2] AND quux IN [1, 2] AND eaea NOT IN [3]', + 'hitsPerPage' => $builder->limit, + ]))->andReturn([]); + + $engine->search($builder); + } + + public function test_where_in_conditions_are_applied_without_other_conditions() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $builder = new Builder(new SearchableUser, ''); + $builder->whereIn('qux', [1, 2]); + $builder->whereIn('quux', [1, 2]); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ + 'filter' => 'qux IN [1, 2] AND quux IN [1, 2]', + 'hitsPerPage' => $builder->limit, + ]))->andReturn([]); + + $engine->search($builder); + } + + public function test_where_not_in_conditions_are_applied_without_other_conditions() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $builder = new Builder(new SearchableUser, ''); + $builder->whereIn('qux', [1, 2]); + $builder->whereIn('quux', [1, 2]); + $builder->whereNotIn('eaea', [3]); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ + 'filter' => 'qux IN [1, 2] AND quux IN [1, 2] AND eaea NOT IN [3]', + 'hitsPerPage' => $builder->limit, + ]))->andReturn([]); + + $engine->search($builder); + } + + public function test_empty_where_in_conditions_are_applied_correctly() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $builder = new Builder(new SearchableUser, ''); + $builder->where('foo', 'bar'); + $builder->where('bar', 'baz'); + $builder->whereIn('qux', []); + + $this->client->shouldReceive('index')->once()->with('users')->andReturn($index = m::mock(Indexes::class)); + $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ + 'filter' => 'foo="bar" AND bar="baz" AND qux IN []', + 'hitsPerPage' => $builder->limit, + ]))->andReturn([]); + + $engine->search($builder); + } + + public function test_delete_all_indexes_works_with_pagination() + { + $engine = $this->app->make(EngineManager::class)->engine(); + + $this->client->shouldReceive('getIndexes')->andReturn($indexesResults = m::mock(IndexesResults::class)); + + $indexesResults->shouldReceive('getResults')->once(); + + $engine->deleteAllIndexes(); + } +} diff --git a/tests/Feature/Jobs/MakeSearchableTest.php b/tests/Feature/Jobs/MakeSearchableTest.php new file mode 100644 index 00000000..958e49a2 --- /dev/null +++ b/tests/Feature/Jobs/MakeSearchableTest.php @@ -0,0 +1,33 @@ +create(); + + $job = new MakeSearchable($collection = Collection::make([$model])); + + $this->app->make('scout.spied')->shouldReceive('update')->with($collection)->once(); + + $job->handle(); + } +} diff --git a/tests/Feature/Jobs/RemovableScoutCollectionTest.php b/tests/Feature/Jobs/RemovableScoutCollectionTest.php new file mode 100644 index 00000000..e03098c9 --- /dev/null +++ b/tests/Feature/Jobs/RemovableScoutCollectionTest.php @@ -0,0 +1,55 @@ +make(['id' => 1]), + SearchableUserFactory::new()->make(['id' => 2]), + ]); + + $this->assertEquals([1, 2], $collection->getQueueableIds()); + } + + public function test_get_queuable_ids_resolves_custom_scout_keys() + { + $collection = RemoveableScoutCollection::make([ + ChirpFactory::new()->make(['scout_id' => 'custom-key.1']), + ChirpFactory::new()->make(['scout_id' => 'custom-key.2']), + ChirpFactory::new()->make(['scout_id' => 'custom-key.3']), + ChirpFactory::new()->make(['scout_id' => 'custom-key.4']), + ]); + + $this->assertEquals([ + 'custom-key.1', + 'custom-key.2', + 'custom-key.3', + 'custom-key.4', + ], $collection->getQueueableIds()); + } + + public function test_removeable_scout_collection_returns_scout_keys() + { + $collection = RemoveableScoutCollection::make([ + ChirpFactory::new()->make(['scout_id' => '1234']), + ChirpFactory::new()->make(['scout_id' => '2345']), + SearchableUserFactory::new()->make(['id' => 3456]), + SearchableUserFactory::new()->make(['id' => 7891]), + ]); + + $this->assertEquals([ + '1234', + '2345', + 3456, + 7891, + ], $collection->getQueueableIds()); + } +} diff --git a/tests/Feature/Jobs/RemoveFromSearchTest.php b/tests/Feature/Jobs/RemoveFromSearchTest.php new file mode 100644 index 00000000..fbf1dafa --- /dev/null +++ b/tests/Feature/Jobs/RemoveFromSearchTest.php @@ -0,0 +1,67 @@ +create(); + + $job = new RemoveFromSearch($models = RemoveableScoutCollection::make([$model])); + + $this->app->make('scout.spied')->shouldReceive('delete')->with(m::type(RemoveableScoutCollection::class))->once(); + + $job->handle(); + } + + public function test_models_are_deserialized_without_the_database() + { + $model = SearchableUserFactory::new()->create(['id' => 1234]); + + $job = new RemoveFromSearch($models = RemoveableScoutCollection::make([$model])); + + $job = unserialize(serialize($job)); + + $this->assertInstanceOf(RemoveableScoutCollection::class, $job->models); + $this->assertCount(1, $job->models); + $this->assertInstanceOf(SearchableUser::class, $job->models->first()); + $this->assertSame(1234, $job->models->first()->getScoutKey()); + } + + public function test_models_are_deserialized_without_the_database_using_custom_scout_key() + { + $model = ChirpFactory::new()->create(['scout_id' => $uuid = Str::uuid()]); + + $job = new RemoveFromSearch($models = RemoveableScoutCollection::make([$model])); + + $job = unserialize(serialize($job)); + + $this->assertInstanceOf(RemoveableScoutCollection::class, $job->models); + $this->assertCount(1, $job->models); + $this->assertInstanceOf(Chirp::class, $job->models->first()); + $this->assertEquals($uuid, $job->models->first()->getScoutKey()); + $this->assertEquals('scout_id', $job->models->first()->getScoutKeyName()); + } +} diff --git a/tests/Feature/ModelObserverTest.php b/tests/Feature/ModelObserverTest.php new file mode 100644 index 00000000..313f5a69 --- /dev/null +++ b/tests/Feature/ModelObserverTest.php @@ -0,0 +1,224 @@ +createQuietly(['name' => 'Laravel']); + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldReceive('update')->once(); + }); + + $model->name = 'Laravel Scout'; + $model->save(); + } + + public function test_saved_handler_doesnt_make_model_searchable_when_search_shouldnt_update() + { + $_ENV['user.searchIndexShouldBeUpdated'] = false; + + $model = SearchableUserFactory::new()->createQuietly(['name' => 'Laravel']); + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldNotReceive('update'); + }); + + $model->name = 'Laravel Scout'; + $model->save(); + + unset($_ENV['user.searchIndexShouldBeUpdated']); + } + + public function test_saved_handler_doesnt_make_model_searchable_when_disabled() + { + $model = SearchableUserFactory::new()->createQuietly(['name' => 'Laravel']); + + ModelObserver::disableSyncingFor(SearchableUser::class); + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldNotReceive('update'); + }); + + $model->name = 'Laravel Scout'; + $model->save(); + + ModelObserver::enableSyncingFor(SearchableUser::class); + } + + public function test_saved_handler_makes_model_unsearchable_when_disabled_per_model_rule() + { + $_ENV['user.shouldBeSearchable'] = false; + + $model = SearchableUserFactory::new()->createQuietly(['name' => 'Laravel']); + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldNotReceive('update'); + }); + + $model->name = 'Laravel Scout'; + $model->save(); + + unset($_ENV['user.shouldBeSearchable']); + } + + public function saved_handler_doesnt_make_model_unsearchable_when_disabled_per_model_rule_and_already_unsearchable() + { + $_ENV['user.wasSearchableBeforeUpdate'] = false; + $_ENV['user.shouldBeSearchable'] = false; + + $model = SearchableUserFactory::new()->createQuietly(['name' => 'Laravel']); + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldNotReceive('update'); + }); + + $model->name = 'Laravel Scout'; + $model->save(); + + unset($_ENV['user.shouldBeSearchable'], $_ENV['user.wasSearchableBeforeUpdate']); + } + + public function test_deleted_handler_doesnt_make_model_unsearchable_when_already_unsearchable() + { + $_ENV['user.wasSearchableBeforeDelete'] = false; + + $model = SearchableUserFactory::new()->createQuietly(); + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldNotReceive('delete'); + }); + + $model->delete(); + + unset($_ENV['user.wasSearchableBeforeDelete']); + } + + public function test_deleted_handler_makes_model_unsearchable() + { + $_ENV['user.wasSearchableBeforeDelete'] = true; + + $model = SearchableUserFactory::new()->createQuietly(); + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldReceive('delete')->once(); + }); + + $model->forceDelete(); + + unset($_ENV['user.wasSearchableBeforeDelete']); + } + + public function test_deleted_handler_on_soft_delete_model_makes_model_unsearchable() + { + $model = ChirpFactory::new()->createQuietly(); + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldReceive('delete')->once(); + }); + + $model->delete(); + } + + public function test_update_on_sensitive_attributes_triggers_search() + { + $_ENV['user.searchIndexShouldBeUpdated'] = function ($model) { + $sensitiveAttributeKeys = ['name', 'email']; + + return collect($model->getDirty())->keys() + ->intersect($sensitiveAttributeKeys) + ->isNotEmpty(); + }; + + $model = SearchableUserFactory::new()->createQuietly([ + 'name' => 'taylor Otwell', + 'remember_token' => 123, + 'password' => 'secret', + ]); + + $model->password = 'extremelySecurePassword'; + $model->name = 'Taylor'; + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldReceive('update')->once(); + }); + + $model->save(); + + unset($_ENV['user.searchIndexShouldBeUpdated']); + } + + public function test_update_on_non_sensitive_attributes_doesnt_trigger_search() + { + $_ENV['user.searchIndexShouldBeUpdated'] = function ($model) { + $sensitiveAttributeKeys = ['name', 'email']; + + return collect($model->getDirty())->keys() + ->intersect($sensitiveAttributeKeys) + ->isNotEmpty(); + }; + + $model = SearchableUserFactory::new()->createQuietly([ + 'name' => 'taylor Otwell', + 'remember_token' => 123, + 'password' => 'secret', + ]); + + $model->password = 'extremelySecurePassword'; + $model->remember_token = 456; + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldNotReceive('update'); + $scout->shouldNotReceive('delete'); + }); + + $model->save(); + + unset($_ENV['user.searchIndexShouldBeUpdated']); + } + + public function test_unsearchable_should_be_called_when_deleting() + { + $_ENV['user.searchIndexShouldBeUpdated'] = function ($model) { + $sensitiveAttributeKeys = ['name', 'email']; + + return collect($model->getDirty())->keys() + ->intersect($sensitiveAttributeKeys) + ->isNotEmpty(); + }; + + $model = SearchableUserFactory::new()->createQuietly([ + 'name' => 'taylor Otwell', + 'remember_token' => 123, + 'password' => 'secret', + ]); + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldNotReceive('update'); + $scout->shouldReceive('delete')->once(); + }); + + $model->delete(); + + unset($_ENV['user.searchIndexShouldBeUpdated']); + } +} diff --git a/tests/Feature/ModelObserverWithSoftDeletesTest.php b/tests/Feature/ModelObserverWithSoftDeletesTest.php new file mode 100644 index 00000000..5dd26dd0 --- /dev/null +++ b/tests/Feature/ModelObserverWithSoftDeletesTest.php @@ -0,0 +1,55 @@ +createQuietly(); + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldReceive('delete')->once(); + }); + + $model->forceDelete(); + } + + public function test_deleted_handler_makes_model_searchable_when_it_should_be_searchable() + { + $model = ChirpFactory::new()->createQuietly(); + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldReceive('update')->once(); + }); + + $model->delete(); + } + + public function test_restored_handler_makes_model_searchable() + { + $model = ChirpFactory::new()->createQuietly([ + 'deleted_at' => now(), + ]); + + tap($this->app->make('scout.spied'), function ($scout) { + $scout->shouldReceive('update')->twice(); + }); + + $model->restore(); + } +} diff --git a/tests/Feature/SearchableTest.php b/tests/Feature/SearchableTest.php index 638c93a7..908f283b 100644 --- a/tests/Feature/SearchableTest.php +++ b/tests/Feature/SearchableTest.php @@ -19,11 +19,11 @@ class SearchableTest extends TestCase public function test_searchable_using_update_is_called_on_collection() { $collection = m::mock(); - $collection->shouldReceive('isEmpty')->andReturn(false); - $collection->shouldReceive('first->makeSearchableUsing')->with($collection)->andReturn($collection); - $collection->shouldReceive('first->searchableUsing->update')->with($collection); + $collection->shouldReceive('isEmpty')->once()->andReturn(false); + $collection->shouldReceive('first->makeSearchableUsing')->with($collection)->once()->andReturn($collection); + $collection->shouldReceive('first->searchableUsing->update')->with($collection)->once(); - $model = new SearchableModel(); + $model = new SearchableModel; $model->queueMakeSearchable($collection); } @@ -45,7 +45,7 @@ public function test_overridden_make_searchable_is_dispatched() Scout::makeSearchableUsing(OverriddenMakeSearchable::class); $collection = m::mock(); - $collection->shouldReceive('isEmpty')->andReturn(false); + $collection->shouldReceive('isEmpty')->once()->andReturn(false); $collection->shouldReceive('first->syncWithSearchUsingQueue'); $collection->shouldReceive('first->syncWithSearchUsing'); @@ -58,7 +58,7 @@ public function test_overridden_make_searchable_is_dispatched() public function test_searchable_using_delete_is_called_on_collection() { $collection = m::mock(); - $collection->shouldReceive('isEmpty')->andReturn(false); + $collection->shouldReceive('isEmpty')->once()->andReturn(false); $collection->shouldReceive('first->searchableUsing->delete')->with($collection); $model = new SearchableModel; @@ -68,7 +68,7 @@ public function test_searchable_using_delete_is_called_on_collection() public function test_searchable_using_delete_is_not_called_on_empty_collection() { $collection = m::mock(); - $collection->shouldReceive('isEmpty')->andReturn(true); + $collection->shouldReceive('isEmpty')->once()->andReturn(true); $collection->shouldNotReceive('first->searchableUsing->delete'); $model = new SearchableModel; @@ -83,7 +83,7 @@ public function test_overridden_remove_from_search_is_dispatched() Scout::removeFromSearchUsing(OverriddenRemoveFromSearch::class); $collection = m::mock(); - $collection->shouldReceive('isEmpty')->andReturn(false); + $collection->shouldReceive('isEmpty')->once()->andReturn(false); $collection->shouldReceive('first->syncWithSearchUsingQueue'); $collection->shouldReceive('first->syncWithSearchUsing'); @@ -105,11 +105,11 @@ public function test_was_searchable_on_model_without_soft_deletes() public function test_it_queries_searchable_models_by_their_ids_with_integer_key_type() { $model = M::mock(SearchableModel::class)->makePartial(); - $model->shouldReceive('newQuery')->andReturnSelf(); - $model->shouldReceive('getScoutKeyType')->andReturn('int'); - $model->shouldReceive('getScoutKeyName')->andReturn('id'); - $model->shouldReceive('qualifyColumn')->with('id')->andReturn('qualified_id'); - $model->shouldReceive('whereIntegerInRaw')->with('qualified_id', [1, 2, 3])->andReturnSelf(); + $model->shouldReceive('newQuery')->once()->andReturnSelf(); + $model->shouldReceive('getScoutKeyType')->once()->andReturn('int'); + $model->shouldReceive('getScoutKeyName')->once()->andReturn('id'); + $model->shouldReceive('qualifyColumn')->with('id')->once()->andReturn('qualified_id'); + $model->shouldReceive('whereIntegerInRaw')->with('qualified_id', [1, 2, 3])->once()->andReturnSelf(); $scoutBuilder = M::mock(\Laravel\Scout\Builder::class); $scoutBuilder->queryCallback = null; @@ -120,11 +120,11 @@ public function test_it_queries_searchable_models_by_their_ids_with_integer_key_ public function test_it_queries_searchable_models_by_their_ids_with_string_key_type() { $model = M::mock(SearchableModel::class)->makePartial(); - $model->shouldReceive('newQuery')->andReturnSelf(); - $model->shouldReceive('getScoutKeyType')->andReturn('string'); - $model->shouldReceive('getScoutKeyName')->andReturn('id'); - $model->shouldReceive('qualifyColumn')->with('id')->andReturn('qualified_id'); - $model->shouldReceive('whereIn')->with('qualified_id', [1, 2, 3])->andReturnSelf(); + $model->shouldReceive('newQuery')->once()->andReturnSelf(); + $model->shouldReceive('getScoutKeyType')->once()->andReturn('string'); + $model->shouldReceive('getScoutKeyName')->once()->andReturn('id'); + $model->shouldReceive('qualifyColumn')->with('id')->once()->andReturn('qualified_id'); + $model->shouldReceive('whereIn')->with('qualified_id', [1, 2, 3])->once()->andReturnSelf(); $scoutBuilder = M::mock(\Laravel\Scout\Builder::class); $scoutBuilder->queryCallback = null; @@ -169,20 +169,24 @@ class ModelStubForMakeAllSearchable extends SearchableModel { public function newQuery() { - $mock = m::mock(Builder::class); + $mock = m::spy(Builder::class); $mock->shouldReceive('when') - ->with(true, m::type('Closure')) - ->andReturnUsing(function ($condition, $callback) use ($mock) { - $callback($mock); + ->with(true, m::type('Closure')) + ->once() + ->andReturnUsing(function ($condition, $callback) use ($mock) { + $callback($mock); - return $mock; - }); + return $mock; + }); $mock->shouldReceive('orderBy') ->with('model_stub_for_make_all_searchables.id') - ->andReturnSelf() - ->shouldReceive('searchable'); + ->once() + ->andReturnSelf(); + + $mock->shouldReceive('searchable') + ->once(); $mock->shouldReceive('when')->andReturnSelf(); diff --git a/tests/Fixtures/EmptySearchableModel.php b/tests/Fixtures/EmptySearchableModel.php deleted file mode 100644 index 6ab93d29..00000000 --- a/tests/Fixtures/EmptySearchableModel.php +++ /dev/null @@ -1,11 +0,0 @@ -other_id; - } - - public function getScoutKeyName() - { - return 'other_id'; - } -} diff --git a/tests/Fixtures/SearchableModelWithSensitiveAttributes.php b/tests/Fixtures/SearchableModelWithSensitiveAttributes.php deleted file mode 100644 index 6601b185..00000000 --- a/tests/Fixtures/SearchableModelWithSensitiveAttributes.php +++ /dev/null @@ -1,35 +0,0 @@ -getDirty())->keys() - ->intersect($sensitiveAttributeKeys) - ->isNotEmpty(); - } -} diff --git a/tests/Fixtures/SearchableModelWithSoftDeletes.php b/tests/Fixtures/SearchableModelWithSoftDeletes.php index ed4a4260..a82234cf 100644 --- a/tests/Fixtures/SearchableModelWithSoftDeletes.php +++ b/tests/Fixtures/SearchableModelWithSoftDeletes.php @@ -8,8 +8,8 @@ class SearchableModelWithSoftDeletes extends Model { - use SoftDeletes; use Searchable; + use SoftDeletes; /** * The attributes that are mass assignable. diff --git a/tests/Fixtures/SearchableModelWithUnloadedValue.php b/tests/Fixtures/SearchableModelWithUnloadedValue.php deleted file mode 100644 index 1526adfe..00000000 --- a/tests/Fixtures/SearchableModelWithUnloadedValue.php +++ /dev/null @@ -1,28 +0,0 @@ - $this->unloadedValue, - ]; - } - - public function makeSearchableUsing(Collection $models) - { - return $models->each( - fn ($model) => $model->unloadedValue = 'loaded', - ); - } -} diff --git a/tests/Fixtures/SearchableUserDatabaseModel.php b/tests/Fixtures/SearchableUserDatabaseModel.php deleted file mode 100644 index 5be01b02..00000000 --- a/tests/Fixtures/SearchableUserDatabaseModel.php +++ /dev/null @@ -1,27 +0,0 @@ - $this->id, - 'name' => $this->name, - 'email' => $this->email, - ]; - } -} diff --git a/tests/Fixtures/SearchableUserModel.php b/tests/Fixtures/SearchableUserModel.php deleted file mode 100644 index 49eef0f0..00000000 --- a/tests/Fixtures/SearchableUserModel.php +++ /dev/null @@ -1,13 +0,0 @@ - strrev($this->name), - ]; - } -} diff --git a/tests/Fixtures/SoftDeletedEmptySearchableModel.php b/tests/Fixtures/SoftDeletedEmptySearchableModel.php deleted file mode 100644 index 78bc60a8..00000000 --- a/tests/Fixtures/SoftDeletedEmptySearchableModel.php +++ /dev/null @@ -1,21 +0,0 @@ - 1]; - } -} diff --git a/tests/Fixtures/User.php b/tests/Fixtures/User.php deleted file mode 100644 index 908961f9..00000000 --- a/tests/Fixtures/User.php +++ /dev/null @@ -1,24 +0,0 @@ - - */ - public function toSearchableArray(): array - { - return [ - 'id' => (int) $this->id, - 'name' => $this->name, - ]; - } -} diff --git a/tests/Integration/AlgoliaSearchableTest.php b/tests/Integration/AlgoliaSearchableTest.php index 76ce55d0..2fb66973 100644 --- a/tests/Integration/AlgoliaSearchableTest.php +++ b/tests/Integration/AlgoliaSearchableTest.php @@ -2,8 +2,8 @@ namespace Laravel\Scout\Tests\Integration; -use Laravel\Scout\Tests\Fixtures\User; use Orchestra\Testbench\Attributes\RequiresEnv; +use Workbench\App\Models\SearchableUser; /** * @group algolia @@ -42,7 +42,7 @@ protected function defineDatabaseMigrations() */ protected function afterRefreshingDatabase() { - $this->importScoutIndexFrom(User::class); + $this->importScoutIndexFrom(SearchableUser::class); } public function test_it_can_use_basic_search() diff --git a/tests/Integration/MeilisearchSearchableTest.php b/tests/Integration/MeilisearchSearchableTest.php index 0881fac1..e4caf7a8 100644 --- a/tests/Integration/MeilisearchSearchableTest.php +++ b/tests/Integration/MeilisearchSearchableTest.php @@ -5,12 +5,12 @@ use Illuminate\Database\Eloquent\Collection; use Laravel\Scout\Builder; use Laravel\Scout\Engines\MeilisearchEngine; -use Laravel\Scout\Tests\Fixtures\User; use Laravel\Scout\Tests\Fixtures\VersionableModel; use Meilisearch\Client; use Meilisearch\Endpoints\Indexes; use Mockery as m; use Orchestra\Testbench\Attributes\RequiresEnv; +use Workbench\App\Models\SearchableUser; /** * @group meilisearch @@ -48,7 +48,7 @@ protected function defineScoutDatabaseMigrations() { $this->baseDefineScoutDatabaseMigrations(); - $this->importScoutIndexFrom(User::class); + $this->importScoutIndexFrom(SearchableUser::class); } public function test_it_can_use_basic_search() @@ -163,7 +163,7 @@ public function test_uses_different_indexes() $index->shouldReceive('rawSearch')->once()->andReturn([]); $engine = new MeilisearchEngine($client); - $builder = new Builder(new VersionableModel(), ''); + $builder = new Builder(new VersionableModel, ''); $engine->search($builder); } diff --git a/tests/Integration/SearchableTests.php b/tests/Integration/SearchableTests.php index ef25c85f..7efccd30 100644 --- a/tests/Integration/SearchableTests.php +++ b/tests/Integration/SearchableTests.php @@ -4,8 +4,8 @@ use Illuminate\Database\Eloquent\Factories\Sequence; use Illuminate\Support\LazyCollection; -use Laravel\Scout\Tests\Fixtures\User; -use Orchestra\Testbench\Factories\UserFactory; +use Workbench\App\Models\SearchableUser; +use Workbench\Database\Factories\UserFactory; trait SearchableTests { @@ -17,6 +17,13 @@ trait SearchableTests */ protected function defineScoutEnvironment($app) { + $_ENV['user.toSearchableArray'] = function ($model) { + return [ + 'id' => (int) $model->id, + 'name' => $model->name, + ]; + }; + $app['config']->set('scout.driver', static::scoutDriver()); } @@ -63,24 +70,24 @@ protected function defineScoutDatabaseMigrations(): void protected function itCanUseBasicSearch() { - return User::search('lar')->take(10)->get(); + return SearchableUser::search('lar')->take(10)->get(); } protected function itCanUseBasicSearchWithQueryCallback() { - return User::search('lar')->take(10)->query(function ($query) { + return SearchableUser::search('lar')->take(10)->query(function ($query) { return $query->whereNotNull('email_verified_at'); })->get(); } protected function itCanUseBasicSearchToFetchKeys() { - return User::search('lar')->take(10)->keys(); + return SearchableUser::search('lar')->take(10)->keys(); } protected function itCanUseBasicSearchWithQueryCallbackToFetchKeys() { - return User::search('lar')->take(10)->query(function ($query) { + return SearchableUser::search('lar')->take(10)->query(function ($query) { return $query->whereNotNull('email_verified_at'); })->keys(); } @@ -88,8 +95,8 @@ protected function itCanUseBasicSearchWithQueryCallbackToFetchKeys() protected function itCanUsePaginatedSearch() { return [ - User::search('lar')->take(10)->paginate(5, 'page', 1), - User::search('lar')->take(10)->paginate(5, 'page', 2), + SearchableUser::search('lar')->take(10)->paginate(5, 'page', 1), + SearchableUser::search('lar')->take(10)->paginate(5, 'page', 2), ]; } @@ -100,16 +107,17 @@ protected function itCanUsePaginatedSearchWithQueryCallback() }; return [ - User::search('lar')->take(10)->query($queryCallback)->paginate(5, 'page', 1), - User::search('lar')->take(10)->query($queryCallback)->paginate(5, 'page', 2), + SearchableUser::search('lar')->take(10)->query($queryCallback)->paginate(5, 'page', 1), + SearchableUser::search('lar')->take(10)->query($queryCallback)->paginate(5, 'page', 2), ]; } protected function itCanUsePaginatedSearchWithEmptyQueryCallback() { $queryCallback = function ($query) { + // }; - return User::search('*')->query($queryCallback)->paginate(); + return SearchableUser::search('*')->query($queryCallback)->paginate(); } } diff --git a/tests/Integration/TestCase.php b/tests/Integration/TestCase.php index 73f6be09..5722e37f 100644 --- a/tests/Integration/TestCase.php +++ b/tests/Integration/TestCase.php @@ -24,8 +24,6 @@ protected function importScoutIndexFrom($model = null) /** * Clean up the testing environment before the next test case. - * - * @return void */ public static function tearDownAfterClass(): void { diff --git a/tests/Integration/TypesenseSearchableTest.php b/tests/Integration/TypesenseSearchableTest.php index 237bd468..1d8cf557 100644 --- a/tests/Integration/TypesenseSearchableTest.php +++ b/tests/Integration/TypesenseSearchableTest.php @@ -2,8 +2,8 @@ namespace Laravel\Scout\Tests\Integration; -use Laravel\Scout\Tests\Fixtures\User; use Orchestra\Testbench\Attributes\RequiresEnv; +use Workbench\App\Models\SearchableUser; /** * @group typesense @@ -42,7 +42,7 @@ protected function defineDatabaseMigrations() */ protected function afterRefreshingDatabase() { - $this->importScoutIndexFrom(User::class); + $this->importScoutIndexFrom(SearchableUser::class); } public function test_it_can_use_basic_search() diff --git a/tests/Unit/Algolia3EngineTest.php b/tests/Unit/Algolia3EngineTest.php index 2b486bed..337b92c7 100644 --- a/tests/Unit/Algolia3EngineTest.php +++ b/tests/Unit/Algolia3EngineTest.php @@ -4,16 +4,12 @@ use Algolia\AlgoliaSearch\SearchClient; use Illuminate\Container\Container; -use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Config; use Illuminate\Support\LazyCollection; use Laravel\Scout\Builder; -use Laravel\Scout\EngineManager; use Laravel\Scout\Engines\Algolia3Engine; -use Laravel\Scout\Jobs\RemoveFromSearch; -use Laravel\Scout\Tests\Fixtures\EmptySearchableModel; use Laravel\Scout\Tests\Fixtures\SearchableModel; -use Laravel\Scout\Tests\Fixtures\SoftDeletedEmptySearchableModel; use Mockery as m; use PHPUnit\Framework\TestCase; use stdClass; @@ -32,156 +28,13 @@ protected function tearDown(): void m::close(); } - public function test_update_adds_objects_to_index() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldReceive('saveObjects')->with([[ - 'id' => 1, - 'objectID' => 1, - ]]); - - $engine = new Algolia3Engine($client); - $engine->update(Collection::make([new SearchableModel])); - } - - public function test_delete_removes_objects_to_index() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldReceive('deleteObjects')->with([1]); - - $engine = new Algolia3Engine($client); - $engine->delete(Collection::make([new SearchableModel(['id' => 1])])); - } - - public function test_delete_removes_objects_to_index_with_a_custom_search_key() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('deleteObjects')->once()->with(['my-algolia-key.5']); - - $engine = new Algolia3Engine($client); - $engine->delete(Collection::make([new Algolia3CustomKeySearchableModel(['id' => 5])])); - } - - public function test_delete_with_removeable_scout_collection_using_custom_search_key() - { - $job = new RemoveFromSearch(Collection::make([ - new Algolia3CustomKeySearchableModel(['id' => 5]), - ])); - - $job = unserialize(serialize($job)); - - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldReceive('deleteObjects')->once()->with(['my-algolia-key.5']); - - $engine = new Algolia3Engine($client); - $engine->delete($job->models); - } - - public function test_remove_from_search_job_uses_custom_search_key() - { - $job = new RemoveFromSearch(Collection::make([ - new Algolia3CustomKeySearchableModel(['id' => 5]), - ])); - - $job = unserialize(serialize($job)); - - Container::getInstance()->bind(EngineManager::class, function () { - $engine = m::mock(Algolia3Engine::class); - - $engine->shouldReceive('delete')->once()->with(m::on(function ($collection) { - $keyName = ($model = $collection->first())->getScoutKeyName(); - - return $model->getAttributes()[$keyName] === 'my-algolia-key.5'; - })); - - $manager = m::mock(EngineManager::class); - - $manager->shouldReceive('engine')->andReturn($engine); - - return $manager; - }); - - $job->handle(); - } - - public function test_search_sends_correct_parameters_to_algolia() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldReceive('search')->with('zonda', [ - 'numericFilters' => ['foo=1'], - ]); - - $engine = new Algolia3Engine($client); - $builder = new Builder(new SearchableModel, 'zonda'); - $builder->where('foo', 1); - $engine->search($builder); - } - - public function test_search_sends_correct_parameters_to_algolia_for_where_in_search() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldReceive('search')->with('zonda', [ - 'numericFilters' => ['foo=1', ['bar=1', 'bar=2']], - ]); - - $engine = new Algolia3Engine($client); - $builder = new Builder(new SearchableModel, 'zonda'); - $builder->where('foo', 1)->whereIn('bar', [1, 2]); - $engine->search($builder); - } - - public function test_search_sends_correct_parameters_to_algolia_for_empty_where_in_search() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldReceive('search')->with('zonda', [ - 'numericFilters' => ['foo=1', '0=1'], - ]); - - $engine = new Algolia3Engine($client); - $builder = new Builder(new SearchableModel, 'zonda'); - $builder->where('foo', 1)->whereIn('bar', []); - $engine->search($builder); - } - - public function test_map_correctly_maps_results_to_models() - { - $client = m::mock(SearchClient::class); - $engine = new Algolia3Engine($client); - - $model = m::mock(stdClass::class); - - $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([ - new SearchableModel(['id' => 1, 'name' => 'test']), - ])); - - $builder = m::mock(Builder::class); - - $results = $engine->map($builder, [ - 'nbHits' => 1, - 'hits' => [ - ['objectID' => 1, 'id' => 1, '_rankingInfo' => ['nbTypos' => 0]], - ], - ], $model); - - $this->assertCount(1, $results); - $this->assertEquals(['id' => 1, 'name' => 'test'], $results->first()->toArray()); - $this->assertEquals(['_rankingInfo' => ['nbTypos' => 0]], $results->first()->scoutMetaData()); - } - - public function test_map_method_respects_order() + public function test_lazy_map_method_respects_order() { $client = m::mock(SearchClient::class); $engine = new Algolia3Engine($client); $model = m::mock(stdClass::class); - $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([ + $model->shouldReceive('queryScoutModelsByIds->cursor')->andReturn($models = LazyCollection::make([ new SearchableModel(['id' => 1]), new SearchableModel(['id' => 2]), new SearchableModel(['id' => 3]), @@ -190,7 +43,7 @@ public function test_map_method_respects_order() $builder = m::mock(Builder::class); - $results = $engine->map($builder, ['nbHits' => 4, 'hits' => [ + $results = $engine->lazyMap($builder, ['nbHits' => 4, 'hits' => [ ['objectID' => 1, 'id' => 1], ['objectID' => 2, 'id' => 2], ['objectID' => 4, 'id' => 4], @@ -209,34 +62,13 @@ public function test_map_method_respects_order() ], $results->toArray()); } - public function test_lazy_map_correctly_maps_results_to_models() - { - $client = m::mock(SearchClient::class); - $engine = new Algolia3Engine($client); - - $model = m::mock(stdClass::class); - $model->shouldReceive('queryScoutModelsByIds->cursor')->andReturn($models = LazyCollection::make([ - new SearchableModel(['id' => 1, 'name' => 'test']), - ])); - - $builder = m::mock(Builder::class); - - $results = $engine->lazyMap($builder, ['nbHits' => 1, 'hits' => [ - ['objectID' => 1, 'id' => 1, '_rankingInfo' => ['nbTypos' => 0]], - ]], $model); - - $this->assertCount(1, $results); - $this->assertEquals(['id' => 1, 'name' => 'test'], $results->first()->toArray()); - $this->assertEquals(['_rankingInfo' => ['nbTypos' => 0]], $results->first()->scoutMetaData()); - } - - public function test_lazy_map_method_respects_order() + public function test_map_method_respects_order() { $client = m::mock(SearchClient::class); $engine = new Algolia3Engine($client); $model = m::mock(stdClass::class); - $model->shouldReceive('queryScoutModelsByIds->cursor')->andReturn($models = LazyCollection::make([ + $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([ new SearchableModel(['id' => 1]), new SearchableModel(['id' => 2]), new SearchableModel(['id' => 3]), @@ -245,7 +77,7 @@ public function test_lazy_map_method_respects_order() $builder = m::mock(Builder::class); - $results = $engine->lazyMap($builder, ['nbHits' => 4, 'hits' => [ + $results = $engine->map($builder, ['nbHits' => 4, 'hits' => [ ['objectID' => 1, 'id' => 1], ['objectID' => 2, 'id' => 2], ['objectID' => 4, 'id' => 4], @@ -263,65 +95,4 @@ public function test_lazy_map_method_respects_order() 3 => ['id' => 3], ], $results->toArray()); } - - public function test_a_model_is_indexed_with_a_custom_algolia_key() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldReceive('saveObjects')->with([[ - 'id' => 1, - 'objectID' => 'my-algolia-key.1', - ]]); - - $engine = new Algolia3Engine($client); - $engine->update(Collection::make([new Algolia3CustomKeySearchableModel])); - } - - public function test_a_model_is_removed_with_a_custom_algolia_key() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldReceive('deleteObjects')->with(['my-algolia-key.1']); - - $engine = new Algolia3Engine($client); - $engine->delete(Collection::make([new Algolia3CustomKeySearchableModel(['id' => 1])])); - } - - public function test_flush_a_model_with_a_custom_algolia_key() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldReceive('clearObjects'); - - $engine = new Algolia3Engine($client); - $engine->flush(new Algolia3CustomKeySearchableModel); - } - - public function test_update_empty_searchable_array_does_not_add_objects_to_index() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldNotReceive('saveObjects'); - - $engine = new Algolia3Engine($client); - $engine->update(Collection::make([new EmptySearchableModel])); - } - - public function test_update_empty_searchable_array_from_soft_deleted_model_does_not_add_objects_to_index() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock('StdClass')); - $index->shouldNotReceive('saveObjects'); - - $engine = new Algolia3Engine($client, true); - $engine->update(Collection::make([new SoftDeletedEmptySearchableModel])); - } -} - -class Algolia3CustomKeySearchableModel extends SearchableModel -{ - public function getScoutKey() - { - return 'my-algolia-key.'.$this->getKey(); - } } diff --git a/tests/Unit/Algolia4EngineTest.php b/tests/Unit/Algolia4EngineTest.php index 49da1075..965878db 100644 --- a/tests/Unit/Algolia4EngineTest.php +++ b/tests/Unit/Algolia4EngineTest.php @@ -8,14 +8,10 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\LazyCollection; use Laravel\Scout\Builder; -use Laravel\Scout\EngineManager; use Laravel\Scout\Engines\Algolia4Engine; -use Laravel\Scout\Jobs\RemoveFromSearch; -use Laravel\Scout\Tests\Fixtures\EmptySearchableModel; use Laravel\Scout\Tests\Fixtures\SearchableModel; -use Laravel\Scout\Tests\Fixtures\SoftDeletedEmptySearchableModel; use Mockery as m; -use PHPUnit\Framework\TestCase; +use Orchestra\Testbench\TestCase; use stdClass; class Algolia4EngineTest extends TestCase @@ -29,159 +25,17 @@ protected function setUp(): void protected function tearDown(): void { Container::getInstance()->flush(); - m::close(); - } - - public function test_update_adds_objects_to_index() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldReceive('saveObjects')->with([[ - 'id' => 1, - 'objectID' => 1, - ]]); - - $engine = new Algolia4Engine($client); - $engine->update(Collection::make([new SearchableModel])); - } - - public function test_delete_removes_objects_to_index() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('deleteObjects')->with('table', [1]); - - $engine = new Algolia4Engine($client); - $engine->delete(Collection::make([new SearchableModel(['id' => 1])])); - } - - public function test_delete_removes_objects_to_index_with_a_custom_search_key() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('deleteObjects')->once()->with('table', ['my-algolia-key.5']); - - $engine = new Algolia4Engine($client); - $engine->delete(Collection::make([new Algolia4CustomKeySearchableModel(['id' => 5])])); - } - - public function test_delete_with_removeable_scout_collection_using_custom_search_key() - { - $job = new RemoveFromSearch(Collection::make([ - new Algolia4CustomKeySearchableModel(['id' => 5]), - ])); - - $job = unserialize(serialize($job)); - - $client = m::mock(SearchClient::class); - $client->shouldReceive('deleteObjects')->once()->with('table', ['my-algolia-key.5']); - - $engine = new Algolia4Engine($client); - $engine->delete($job->models); - } - - public function test_remove_from_search_job_uses_custom_search_key() - { - $job = new RemoveFromSearch(Collection::make([ - new Algolia4CustomKeySearchableModel(['id' => 5]), - ])); - - $job = unserialize(serialize($job)); - - Container::getInstance()->bind(EngineManager::class, function () { - $engine = m::mock(Algolia4Engine::class); - - $engine->shouldReceive('delete')->once()->with(m::on(function ($collection) { - $keyName = ($model = $collection->first())->getScoutKeyName(); - - return $model->getAttributes()[$keyName] === 'my-algolia-key.5'; - })); - - $manager = m::mock(EngineManager::class); - - $manager->shouldReceive('engine')->andReturn($engine); - - return $manager; - }); - - $job->handle(); - } - - public function test_search_sends_correct_parameters_to_algolia() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('searchSingleIndex')->with( - 'table', - ['query' => 'zonda'], - ['numericFilters' => ['foo=1']] - ); - - $engine = new Algolia4Engine($client); - $builder = new Builder(new SearchableModel, 'zonda'); - $builder->where('foo', 1); - $engine->search($builder); - } - - public function test_search_sends_correct_parameters_to_algolia_for_where_in_search() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('searchSingleIndex')->with( - 'table', - ['query' => 'zonda'], - ['numericFilters' => ['foo=1', ['bar=1', 'bar=2']]] - ); - - $engine = new Algolia4Engine($client); - $builder = new Builder(new SearchableModel, 'zonda'); - $builder->where('foo', 1)->whereIn('bar', [1, 2]); - $engine->search($builder); - } - - public function test_search_sends_correct_parameters_to_algolia_for_empty_where_in_search() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('searchSingleIndex')->with( - 'table', - ['query' => 'zonda'], - ['numericFilters' => ['foo=1', '0=1']] - ); - - $engine = new Algolia4Engine($client); - $builder = new Builder(new SearchableModel, 'zonda'); - $builder->where('foo', 1)->whereIn('bar', []); - $engine->search($builder); - } - - public function test_map_correctly_maps_results_to_models() - { - $client = m::mock(SearchClient::class); - $engine = new Algolia4Engine($client); - - $model = m::mock(stdClass::class); - - $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([ - new SearchableModel(['id' => 1, 'name' => 'test']), - ])); - - $builder = m::mock(Builder::class); - - $results = $engine->map($builder, [ - 'nbHits' => 1, - 'hits' => [ - ['objectID' => 1, 'id' => 1, '_rankingInfo' => ['nbTypos' => 0]], - ], - ], $model); - $this->assertCount(1, $results); - $this->assertEquals(['id' => 1, 'name' => 'test'], $results->first()->toArray()); - $this->assertEquals(['_rankingInfo' => ['nbTypos' => 0]], $results->first()->scoutMetaData()); + m::close(); } - public function test_map_method_respects_order() + public function test_lazy_map_method_respects_order() { $client = m::mock(SearchClient::class); $engine = new Algolia4Engine($client); $model = m::mock(stdClass::class); - $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([ + $model->shouldReceive('queryScoutModelsByIds->cursor')->andReturn($models = LazyCollection::make([ new SearchableModel(['id' => 1]), new SearchableModel(['id' => 2]), new SearchableModel(['id' => 3]), @@ -190,7 +44,7 @@ public function test_map_method_respects_order() $builder = m::mock(Builder::class); - $results = $engine->map($builder, ['nbHits' => 4, 'hits' => [ + $results = $engine->lazyMap($builder, ['nbHits' => 4, 'hits' => [ ['objectID' => 1, 'id' => 1], ['objectID' => 2, 'id' => 2], ['objectID' => 4, 'id' => 4], @@ -209,34 +63,13 @@ public function test_map_method_respects_order() ], $results->toArray()); } - public function test_lazy_map_correctly_maps_results_to_models() - { - $client = m::mock(SearchClient::class); - $engine = new Algolia4Engine($client); - - $model = m::mock(stdClass::class); - $model->shouldReceive('queryScoutModelsByIds->cursor')->andReturn($models = LazyCollection::make([ - new SearchableModel(['id' => 1, 'name' => 'test']), - ])); - - $builder = m::mock(Builder::class); - - $results = $engine->lazyMap($builder, ['nbHits' => 1, 'hits' => [ - ['objectID' => 1, 'id' => 1, '_rankingInfo' => ['nbTypos' => 0]], - ]], $model); - - $this->assertCount(1, $results); - $this->assertEquals(['id' => 1, 'name' => 'test'], $results->first()->toArray()); - $this->assertEquals(['_rankingInfo' => ['nbTypos' => 0]], $results->first()->scoutMetaData()); - } - - public function test_lazy_map_method_respects_order() + public function test_map_method_respects_order() { $client = m::mock(SearchClient::class); $engine = new Algolia4Engine($client); $model = m::mock(stdClass::class); - $model->shouldReceive('queryScoutModelsByIds->cursor')->andReturn($models = LazyCollection::make([ + $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([ new SearchableModel(['id' => 1]), new SearchableModel(['id' => 2]), new SearchableModel(['id' => 3]), @@ -245,7 +78,7 @@ public function test_lazy_map_method_respects_order() $builder = m::mock(Builder::class); - $results = $engine->lazyMap($builder, ['nbHits' => 4, 'hits' => [ + $results = $engine->map($builder, ['nbHits' => 4, 'hits' => [ ['objectID' => 1, 'id' => 1], ['objectID' => 2, 'id' => 2], ['objectID' => 4, 'id' => 4], @@ -263,63 +96,4 @@ public function test_lazy_map_method_respects_order() 3 => ['id' => 3], ], $results->toArray()); } - - public function test_a_model_is_indexed_with_a_custom_algolia_key() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldReceive('saveObjects')->with([[ - 'id' => 1, - 'objectID' => 'my-algolia-key.1', - ]]); - - $engine = new Algolia4Engine($client); - $engine->update(Collection::make([new Algolia4CustomKeySearchableModel])); - } - - public function test_a_model_is_removed_with_a_custom_algolia_key() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('deleteObjects')->with('table', ['my-algolia-key.1']); - - $engine = new Algolia4Engine($client); - $engine->delete(Collection::make([new Algolia4CustomKeySearchableModel(['id' => 1])])); - } - - public function test_flush_a_model_with_a_custom_algolia_key() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('clearObjects')->with('table'); - - $engine = new Algolia4Engine($client); - $engine->flush(new Algolia4CustomKeySearchableModel); - } - - public function test_update_empty_searchable_array_does_not_add_objects_to_index() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class)); - $index->shouldNotReceive('saveObjects'); - - $engine = new Algolia4Engine($client); - $engine->update(Collection::make([new EmptySearchableModel])); - } - - public function test_update_empty_searchable_array_from_soft_deleted_model_does_not_add_objects_to_index() - { - $client = m::mock(SearchClient::class); - $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock('StdClass')); - $index->shouldNotReceive('saveObjects'); - - $engine = new Algolia4Engine($client, true); - $engine->update(Collection::make([new SoftDeletedEmptySearchableModel])); - } -} - -class Algolia4CustomKeySearchableModel extends SearchableModel -{ - public function getScoutKey() - { - return 'my-algolia-key.'.$this->getKey(); - } } diff --git a/tests/Unit/MakeSearchableTest.php b/tests/Unit/MakeSearchableTest.php deleted file mode 100644 index 5d537e00..00000000 --- a/tests/Unit/MakeSearchableTest.php +++ /dev/null @@ -1,28 +0,0 @@ -makePartial(), - ])); - - $model->shouldReceive('searchableUsing->update')->with($collection); - - $job->handle(); - } -} diff --git a/tests/Unit/MeilisearchEngineTest.php b/tests/Unit/MeilisearchEngineTest.php index ff57c7f9..3bd1e7b2 100644 --- a/tests/Unit/MeilisearchEngineTest.php +++ b/tests/Unit/MeilisearchEngineTest.php @@ -7,16 +7,9 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\LazyCollection; use Laravel\Scout\Builder; -use Laravel\Scout\EngineManager; use Laravel\Scout\Engines\MeilisearchEngine; -use Laravel\Scout\Jobs\RemoveFromSearch; -use Laravel\Scout\Tests\Fixtures\EmptySearchableModel; use Laravel\Scout\Tests\Fixtures\SearchableModel; -use Laravel\Scout\Tests\Fixtures\SoftDeletedEmptySearchableModel; use Meilisearch\Client; -use Meilisearch\Contracts\IndexesResults; -use Meilisearch\Endpoints\Indexes; -use Meilisearch\Search\SearchResult; use Mockery as m; use PHPUnit\Framework\TestCase; use stdClass; @@ -35,178 +28,6 @@ protected function tearDown(): void m::close(); } - public function test_update_adds_objects_to_index() - { - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('addDocuments')->with([ - [ - 'id' => 1, - ], - 'id', - ]); - - $engine = new MeilisearchEngine($client); - $engine->update(Collection::make([new SearchableModel()])); - } - - public function test_delete_removes_objects_to_index() - { - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('deleteDocuments')->with([1]); - - $engine = new MeilisearchEngine($client); - $engine->delete(Collection::make([new SearchableModel(['id' => 1])])); - } - - public function test_delete_removes_objects_to_index_with_a_custom_search_key() - { - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('deleteDocuments')->once()->with(['my-meilisearch-key.5']); - - $engine = new MeilisearchEngine($client); - $engine->delete(Collection::make([new MeilisearchCustomKeySearchableModel(['id' => 5])])); - } - - public function test_delete_with_removeable_scout_collection_using_custom_search_key() - { - $job = new RemoveFromSearch(Collection::make([ - new MeilisearchCustomKeySearchableModel(['id' => 5]), - ])); - - $job = unserialize(serialize($job)); - - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('deleteDocuments')->once()->with(['my-meilisearch-key.5']); - - $engine = new MeilisearchEngine($client); - $engine->delete($job->models); - } - - public function test_remove_from_search_job_uses_custom_search_key() - { - $job = new RemoveFromSearch(Collection::make([ - new MeilisearchCustomKeySearchableModel(['id' => 5]), - ])); - - $job = unserialize(serialize($job)); - - Container::getInstance()->bind(EngineManager::class, function () { - $engine = m::mock(MeilisearchEngine::class); - - $engine->shouldReceive('delete')->once()->with(m::on(function ($collection) { - $keyName = ($model = $collection->first())->getScoutKeyName(); - - return $model->getAttributes()[$keyName] === 'my-meilisearch-key.5'; - })); - - $manager = m::mock(EngineManager::class); - - $manager->shouldReceive('engine')->andReturn($engine); - - return $manager; - }); - - $job->handle(); - } - - public function test_search_sends_correct_parameters_to_meilisearch() - { - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('search')->with('mustang', [ - 'filter' => 'foo=1 AND bar=2', - ]); - - $engine = new MeilisearchEngine($client); - $builder = new Builder(new SearchableModel(), 'mustang', function ($meilisearch, $query, $options) { - $options['filter'] = 'foo=1 AND bar=2'; - - return $meilisearch->search($query, $options); - }); - $engine->search($builder); - } - - public function test_search_includes_at_least_scoutKeyName_in_attributesToRetrieve_on_builder_options() - { - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('search')->with('mustang', [ - 'filter' => 'foo=1 AND bar=2', - 'attributesToRetrieve' => ['id', 'foo'], - ]); - - $engine = new MeilisearchEngine($client); - $builder = new Builder(new SearchableModel(), 'mustang', function ($meilisearch, $query, $options) { - $options['filter'] = 'foo=1 AND bar=2'; - - return $meilisearch->search($query, $options); - }); - $builder->options = ['attributesToRetrieve' => ['foo']]; - $engine->search($builder); - } - - public function test_submitting_a_callable_search_with_search_method_returns_array() - { - $builder = new Builder( - new SearchableModel(), - $query = 'mustang', - $callable = function ($meilisearch, $query, $options) { - $options['filter'] = 'foo=1'; - - return $meilisearch->search($query, $options); - } - ); - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('search')->with($query, ['filter' => 'foo=1'])->andReturn(new SearchResult($expectedResult = [ - 'hits' => [], - 'page' => 1, - 'hitsPerPage' => $builder->limit, - 'totalPages' => 1, - 'totalHits' => 0, - 'processingTimeMs' => 1, - 'query' => 'mustang', - ])); - - $engine = new MeilisearchEngine($client); - $result = $engine->search($builder); - - $this->assertSame($expectedResult, $result); - } - - public function test_submitting_a_callable_search_with_raw_search_method_works() - { - $builder = new Builder( - new SearchableModel(), - $query = 'mustang', - $callable = function ($meilisearch, $query, $options) { - $options['filter'] = 'foo=1'; - - return $meilisearch->rawSearch($query, $options); - } - ); - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('rawSearch')->with($query, ['filter' => 'foo=1'])->andReturn($expectedResult = [ - 'hits' => [], - 'page' => 1, - 'hitsPerPage' => $builder->limit, - 'totalPages' => 1, - 'totalHits' => 0, - 'processingTimeMs' => 1, - 'query' => $query, - ]); - - $engine = new MeilisearchEngine($client); - $result = $engine->search($builder); - - $this->assertSame($expectedResult, $result); - } - public function test_map_ids_returns_empty_collection_if_no_hits() { $client = m::mock(Client::class); @@ -257,14 +78,14 @@ public function test_map_ids_returns_correct_values_of_primary_key() public function test_returns_primary_keys_when_custom_array_order_present() { - $engine = m::mock(MeilisearchEngine::class); + $engine = m::spy(MeilisearchEngine::class); $builder = m::mock(Builder::class); $model = m::mock(stdClass::class); - $model->shouldReceive(['getScoutKeyName' => 'custom_key']); + $model->shouldReceive(['getScoutKeyName' => 'custom_key'])->once(); $builder->model = $model; - $engine->shouldReceive('keys')->passthru(); + $engine->shouldReceive('keys')->once()->passthru(); $engine ->shouldReceive('search') @@ -398,243 +219,28 @@ public function test_lazy_map_method_respects_order() ], $results->toArray()); } - public function test_a_model_is_indexed_with_a_custom_meilisearch_key() - { - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('addDocuments')->once()->with([[ - 'meilisearch-key' => 'my-meilisearch-key.5', - 'id' => 5, - ]], 'meilisearch-key'); - - $engine = new MeilisearchEngine($client); - $engine->update(Collection::make([new MeilisearchCustomKeySearchableModel(['id' => 5])])); - } - - public function test_flush_a_model_with_a_custom_meilisearch_key() - { - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('deleteAllDocuments'); - - $engine = new MeilisearchEngine($client); - $engine->flush(new MeilisearchCustomKeySearchableModel()); - } - - public function test_update_empty_searchable_array_does_not_add_documents_to_index() - { - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldNotReceive('addDocuments'); - - $engine = new MeilisearchEngine($client); - $engine->update(Collection::make([new EmptySearchableModel()])); - } - - public function test_pagination_correct_parameters() - { - $perPage = 5; - $page = 2; - - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('search')->with('mustang', [ - 'filter' => 'foo=1', - 'hitsPerPage' => $perPage, - 'page' => $page, - ]); - - $engine = new MeilisearchEngine($client); - $builder = new Builder(new SearchableModel(), 'mustang', function ($meilisearch, $query, $options) { - $options['filter'] = 'foo=1'; - - return $meilisearch->search($query, $options); - }); - $engine->paginate($builder, $perPage, $page); - } - - public function test_pagination_sorted_parameter() - { - $perPage = 5; - $page = 2; - - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('search')->with('mustang', [ - 'filter' => 'foo=1', - 'hitsPerPage' => $perPage, - 'page' => $page, - 'sort' => ['name:asc'], - ]); - - $engine = new MeilisearchEngine($client); - $builder = new Builder(new SearchableModel(), 'mustang', function ($meilisearch, $query, $options) { - $options['filter'] = 'foo=1'; - - return $meilisearch->search($query, $options); - }); - $builder->orderBy('name', 'asc'); - $engine->paginate($builder, $perPage, $page); - } - - public function test_update_empty_searchable_array_from_soft_deleted_model_does_not_add_documents_to_index() - { - $client = m::mock(Client::class); - $client->shouldReceive('index')->with('table')->andReturn(m::mock(Indexes::class)); - $client->shouldReceive('index')->with('table')->andReturn($index = m::mock(Indexes::class)); - $index->shouldNotReceive('addDocuments'); - - $engine = new MeilisearchEngine($client, true); - $engine->update(Collection::make([new SoftDeletedEmptySearchableModel()])); - } - public function test_engine_forwards_calls_to_meilisearch_client() { $client = m::mock(Client::class); - $client->shouldReceive('testMethodOnClient')->once(); + $client->shouldReceive('testMethodOnClient')->once()->andReturn('meilisearch'); $engine = new MeilisearchEngine($client); - $engine->testMethodOnClient(); + $this->assertSame('meilisearch', $engine->testMethodOnClient()); } public function test_updating_empty_eloquent_collection_does_nothing() { $client = m::mock(Client::class); $engine = new MeilisearchEngine($client); - $engine->update(new Collection()); + $engine->update(new Collection); $this->assertTrue(true); } - public function test_performing_search_without_callback_works() - { - $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); - } - - public function test_where_conditions_are_applied() - { - $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([ - 'filter' => 'foo="bar" AND key="value"', - 'hitsPerPage' => $builder->limit, - ]))->andReturn([]); - - $engine = new MeilisearchEngine($client); - $engine->search($builder); - } - - public function test_where_in_conditions_are_applied() - { - $builder = new Builder(new SearchableModel(), ''); - $builder->where('foo', 'bar'); - $builder->where('bar', 'baz'); - $builder->whereIn('qux', [1, 2]); - $builder->whereIn('quux', [1, 2]); - $client = m::mock(Client::class); - $client->shouldReceive('index')->once()->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ - 'filter' => 'foo="bar" AND bar="baz" AND qux IN [1, 2] AND quux IN [1, 2]', - 'hitsPerPage' => $builder->limit, - ]))->andReturn([]); - - $engine = new MeilisearchEngine($client); - $engine->search($builder); - } - - public function test_where_not_in_conditions_are_applied() - { - $builder = new Builder(new SearchableModel(), ''); - $builder->where('foo', 'bar'); - $builder->where('bar', 'baz'); - $builder->whereIn('qux', [1, 2]); - $builder->whereIn('quux', [1, 2]); - $builder->whereNotIn('eaea', [3]); - $client = m::mock(Client::class); - $client->shouldReceive('index')->once()->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ - 'filter' => 'foo="bar" AND bar="baz" AND qux IN [1, 2] AND quux IN [1, 2] AND eaea NOT IN [3]', - 'hitsPerPage' => $builder->limit, - ]))->andReturn([]); - - $engine = new MeilisearchEngine($client); - $engine->search($builder); - } - - public function test_where_in_conditions_are_applied_without_other_conditions() - { - $builder = new Builder(new SearchableModel(), ''); - $builder->whereIn('qux', [1, 2]); - $builder->whereIn('quux', [1, 2]); - $client = m::mock(Client::class); - $client->shouldReceive('index')->once()->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ - 'filter' => 'qux IN [1, 2] AND quux IN [1, 2]', - 'hitsPerPage' => $builder->limit, - ]))->andReturn([]); - - $engine = new MeilisearchEngine($client); - $engine->search($builder); - } - - public function test_where_not_in_conditions_are_applied_without_other_conditions() - { - $builder = new Builder(new SearchableModel(), ''); - $builder->whereIn('qux', [1, 2]); - $builder->whereIn('quux', [1, 2]); - $builder->whereNotIn('eaea', [3]); - $client = m::mock(Client::class); - $client->shouldReceive('index')->once()->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ - 'filter' => 'qux IN [1, 2] AND quux IN [1, 2] AND eaea NOT IN [3]', - 'hitsPerPage' => $builder->limit, - ]))->andReturn([]); - - $engine = new MeilisearchEngine($client); - $engine->search($builder); - } - - public function test_empty_where_in_conditions_are_applied_correctly() - { - $builder = new Builder(new SearchableModel(), ''); - $builder->where('foo', 'bar'); - $builder->where('bar', 'baz'); - $builder->whereIn('qux', []); - $client = m::mock(Client::class); - $client->shouldReceive('index')->once()->andReturn($index = m::mock(Indexes::class)); - $index->shouldReceive('rawSearch')->once()->with($builder->query, array_filter([ - 'filter' => 'foo="bar" AND bar="baz" AND qux IN []', - 'hitsPerPage' => $builder->limit, - ]))->andReturn([]); - - $engine = new MeilisearchEngine($client); - $engine->search($builder); - } - public function test_engine_returns_hits_entry_from_search_response() { - $this->assertTrue(3 === (new MeilisearchEngine(m::mock(Client::class)))->getTotalCount([ + $this->assertTrue((new MeilisearchEngine(m::mock(Client::class)))->getTotalCount([ 'totalHits' => 3, - ])); - } - - public function test_delete_all_indexes_works_with_pagination() - { - $client = m::mock(Client::class); - $client->shouldReceive('getIndexes')->andReturn($indexesResults = m::mock(IndexesResults::class)); - - $indexesResults->shouldReceive('getResults')->once(); - - $engine = new MeilisearchEngine($client); - $engine->deleteAllIndexes(); + ]) === 3); } } diff --git a/tests/Unit/ModelObserverTest.php b/tests/Unit/ModelObserverTest.php deleted file mode 100644 index ce92db4a..00000000 --- a/tests/Unit/ModelObserverTest.php +++ /dev/null @@ -1,182 +0,0 @@ -with('scout.after_commit', m::any())->andReturn(false); - Config::shouldReceive('get')->with('scout.soft_delete', m::any())->andReturn(false); - } - - protected function tearDown(): void - { - m::close(); - } - - public function test_saved_handler_makes_model_searchable() - { - $observer = new ModelObserver; - $model = m::mock(); - $model->shouldReceive('searchIndexShouldBeUpdated')->andReturn(true); - $model->shouldReceive('shouldBeSearchable')->andReturn(true); - $model->shouldReceive('searchable')->once(); - $observer->saved($model); - } - - public function test_saved_handler_doesnt_make_model_searchable_when_search_shouldnt_update() - { - $observer = new ModelObserver; - $model = m::mock(); - $model->shouldReceive('searchIndexShouldBeUpdated')->andReturn(false); - $model->shouldReceive('shouldBeSearchable')->andReturn(true); - $model->shouldReceive('searchable')->never(); - $observer->saved($model); - } - - public function test_saved_handler_doesnt_make_model_searchable_when_disabled() - { - $observer = new ModelObserver; - $model = m::mock(); - $observer->disableSyncingFor(get_class($model)); - $model->shouldReceive('searchable')->never(); - $observer->saved($model); - $observer->enableSyncingFor(get_class($model)); - } - - public function test_saved_handler_makes_model_unsearchable_when_disabled_per_model_rule() - { - $observer = new ModelObserver; - $model = m::mock(); - $model->shouldReceive('searchIndexShouldBeUpdated')->andReturn(true); - $model->shouldReceive('shouldBeSearchable')->andReturn(false); - $model->shouldReceive('wasSearchableBeforeUpdate')->andReturn(true); - $model->shouldReceive('searchable')->never(); - $model->shouldReceive('unsearchable')->once(); - $observer->saved($model); - } - - public function test_saved_handler_doesnt_make_model_unsearchable_when_disabled_per_model_rule_and_already_unsearchable() - { - $observer = new ModelObserver; - $model = m::mock(Model::class); - $model->shouldReceive('searchIndexShouldBeUpdated')->andReturn(true); - $model->shouldReceive('shouldBeSearchable')->andReturn(false); - $model->shouldReceive('wasSearchableBeforeUpdate')->andReturn(false); - $model->shouldReceive('searchable')->never(); - $model->shouldReceive('unsearchable')->never(); - $observer->saved($model); - } - - public function test_deleted_handler_doesnt_make_model_unsearchable_when_already_unsearchable() - { - $observer = new ModelObserver; - $model = m::mock(); - $model->shouldReceive('wasSearchableBeforeDelete')->andReturn(false); - $model->shouldReceive('unsearchable')->never(); - $observer->deleted($model); - } - - public function test_deleted_handler_makes_model_unsearchable() - { - $observer = new ModelObserver; - $model = m::mock(); - $model->shouldReceive('wasSearchableBeforeDelete')->andReturn(true); - $model->shouldReceive('unsearchable')->once(); - $observer->deleted($model); - } - - public function test_deleted_handler_on_soft_delete_model_makes_model_unsearchable() - { - $observer = new ModelObserver; - $model = m::mock(SearchableModelWithSoftDeletes::class); - $model->shouldReceive('wasSearchableBeforeDelete')->andReturn(true); - $model->shouldReceive('searchable')->never(); - $model->shouldReceive('unsearchable')->once(); - $observer->deleted($model); - } - - public function test_update_on_sensitive_attributes_triggers_search() - { - $model = m::mock( - new SearchableModelWithSensitiveAttributes([ - 'first_name' => 'taylor', - 'last_name' => 'Otwell', - 'remember_token' => 123, - 'password' => 'secret', - ]) - )->makePartial(); - - // Let's pretend it's in sync with the database. - $model->syncOriginal(); - - // Update - $model->password = 'extremelySecurePassword'; - $model->first_name = 'Taylor'; - - // Assertions - $model->shouldReceive('searchable')->once(); - $model->shouldReceive('unsearchable')->never(); - - $observer = new ModelObserver; - $observer->saved($model); - } - - public function test_update_on_non_sensitive_attributes_doesnt_trigger_search() - { - $model = m::mock( - new SearchableModelWithSensitiveAttributes([ - 'first_name' => 'taylor', - 'last_name' => 'Otwell', - 'remember_token' => 123, - 'password' => 'secret', - ]) - )->makePartial(); - - // Let's pretend it's in sync with the database. - $model->syncOriginal(); - - // Update - $model->password = 'extremelySecurePassword'; - $model->remember_token = 456; - - // Assertions - $model->shouldReceive('searchable')->never(); - $model->shouldReceive('unsearchable')->never(); - - $observer = new ModelObserver; - $observer->saved($model); - } - - public function test_unsearchable_should_be_called_when_deleting() - { - $model = m::mock( - new SearchableModelWithSensitiveAttributes([ - 'first_name' => 'taylor', - 'last_name' => 'Otwell', - 'remember_token' => 123, - 'password' => 'secret', - ]) - )->makePartial(); - - // Let's pretend it's in sync with the database. - $model->syncOriginal(); - - // Assertions - $model->shouldReceive('searchable')->never(); - $model->shouldReceive('unsearchable')->once(); - - $observer = new ModelObserver; - $observer->deleted($model); - } -} diff --git a/tests/Unit/ModelObserverWithSoftDeletesTest.php b/tests/Unit/ModelObserverWithSoftDeletesTest.php deleted file mode 100644 index 1a124f8f..00000000 --- a/tests/Unit/ModelObserverWithSoftDeletesTest.php +++ /dev/null @@ -1,83 +0,0 @@ -with('scout.after_commit', m::any())->andReturn(false); - Config::shouldReceive('get')->with('scout.soft_delete', m::any())->andReturn(true); - } - - protected function tearDown(): void - { - m::close(); - } - - public function test_deleted_handler_makes_model_unsearchable_when_it_should_not_be_searchable() - { - $observer = new ModelObserver; - $model = m::mock(SearchableModelWithSoftDeletes::class); - $model->shouldReceive('searchShouldUpdate')->never(); // The saved event is forced - $model->shouldReceive('shouldBeSearchable')->andReturn(false); // Should not be searchable - $model->shouldReceive('wasSearchableBeforeDelete')->andReturn(true); - $model->shouldReceive('wasSearchableBeforeUpdate')->andReturn(true); - $model->shouldReceive('searchable')->never(); - $model->shouldReceive('unsearchable')->once(); - $observer->deleted($model); - } - - public function test_deleted_handler_makes_model_searchable_when_it_should_be_searchable() - { - $observer = new ModelObserver; - $model = m::mock(SearchableModelWithSoftDeletes::class); - $model->shouldReceive('searchShouldUpdate')->never(); // The saved event is forced - $model->shouldReceive('shouldBeSearchable')->andReturn(true); // Should be searchable - $model->shouldReceive('wasSearchableBeforeDelete')->andReturn(true); - $model->shouldReceive('searchable')->once(); - $model->shouldReceive('unsearchable')->never(); - $observer->deleted($model); - } - - public function test_restored_handler_makes_model_searchable() - { - $observer = new ModelObserver; - $model = m::mock(SearchableModelWithSoftDeletes::class); - $model->shouldReceive('searchShouldUpdate')->never(); - $model->shouldReceive('shouldBeSearchable')->andReturn(true); - $model->shouldReceive('searchable')->once(); - $model->shouldReceive('unsearchable')->never(); - $observer->restored($model); - } - - public function test_unsearchable_should_be_called_when_deleting() - { - $model = m::mock( - new SearchableModelWithSensitiveAttributes([ - 'first_name' => 'taylor', - 'last_name' => 'Otwell', - 'remember_token' => 123, - 'password' => 'secret', - ]) - )->makePartial(); - - // Let's pretend it's in sync with the database. - $model->syncOriginal(); - - // Assertions - $model->shouldReceive('searchable')->once(); - $model->shouldReceive('unsearchable')->never(); - - $observer = new ModelObserver; - $observer->deleted($model); - } -} diff --git a/tests/Unit/RemoveFromSearchTest.php b/tests/Unit/RemoveFromSearchTest.php deleted file mode 100644 index 4a5132ec..00000000 --- a/tests/Unit/RemoveFromSearchTest.php +++ /dev/null @@ -1,89 +0,0 @@ -with('scout.after_commit', m::any())->andReturn(false); - Config::shouldReceive('get')->with('scout.soft_delete', m::any())->andReturn(false); - } - - protected function tearDown(): void - { - m::close(); - } - - public function test_handle_passes_the_collection_to_engine() - { - $job = new RemoveFromSearch(Collection::make([ - $model = m::mock(), - ])); - - $model->shouldReceive('searchableUsing->delete')->with( - m::on(function ($collection) use ($model) { - return $collection instanceof RemoveableScoutCollection && $collection->first() === $model; - }) - ); - - $job->handle(); - } - - public function test_models_are_deserialized_without_the_database() - { - $job = new RemoveFromSearch(Collection::make([ - $model = new SearchableModel(['id' => 1234]), - ])); - - $job = unserialize(serialize($job)); - - $this->assertInstanceOf(Collection::class, $job->models); - $this->assertCount(1, $job->models); - $this->assertInstanceOf(SearchableModel::class, $job->models->first()); - $this->assertTrue($model->is($job->models->first())); - $this->assertEquals(1234, $job->models->first()->getScoutKey()); - } - - public function test_models_are_deserialized_without_the_database_using_custom_scout_key() - { - $job = new RemoveFromSearch(Collection::make([ - $model = new SearchableModelWithCustomKey(['other_id' => 1234]), - ])); - - $job = unserialize(serialize($job)); - - $this->assertInstanceOf(Collection::class, $job->models); - $this->assertCount(1, $job->models); - $this->assertInstanceOf(SearchableModelWithCustomKey::class, $job->models->first()); - $this->assertTrue($model->is($job->models->first())); - $this->assertEquals(1234, $job->models->first()->getScoutKey()); - $this->assertEquals('other_id', $job->models->first()->getScoutKeyName()); - } - - public function test_removeable_scout_collection_returns_scout_keys() - { - $collection = RemoveableScoutCollection::make([ - new SearchableModelWithCustomKey(['other_id' => 1234]), - new SearchableModelWithCustomKey(['other_id' => 2345]), - new SearchableModel(['id' => 3456]), - new SearchableModel(['id' => 7891]), - ]); - - $this->assertEquals([ - 1234, - 2345, - 3456, - 7891, - ], $collection->getQueueableIds()); - } -} diff --git a/tests/Unit/RemoveableScoutCollectionTest.php b/tests/Unit/RemoveableScoutCollectionTest.php deleted file mode 100644 index 1acef29c..00000000 --- a/tests/Unit/RemoveableScoutCollectionTest.php +++ /dev/null @@ -1,53 +0,0 @@ -with('scout.after_commit', m::any())->andReturn(false); - Config::shouldReceive('get')->with('scout.soft_delete', m::any())->andReturn(false); - } - - public function test_get_queuable_ids() - { - $collection = RemoveableScoutCollection::make([ - new SearchableModel(['id' => 1]), - new SearchableModel(['id' => 2]), - ]); - - $this->assertEquals([1, 2], $collection->getQueueableIds()); - } - - public function test_get_queuable_ids_resolves_custom_scout_keys() - { - $collection = RemoveableScoutCollection::make([ - new SearchCustomKeySearchableModel(['id' => 1]), - new SearchCustomKeySearchableModel(['id' => 2]), - new SearchCustomKeySearchableModel(['id' => 3]), - new SearchCustomKeySearchableModel(['id' => 4]), - ]); - - $this->assertEquals([ - 'custom-key.1', - 'custom-key.2', - 'custom-key.3', - 'custom-key.4', - ], $collection->getQueueableIds()); - } -} - -class SearchCustomKeySearchableModel extends SearchableModel -{ - public function getScoutKey() - { - return 'custom-key.'.$this->getKey(); - } -} diff --git a/tests/Unit/SearchableScopeTest.php b/tests/Unit/SearchableScopeTest.php index 0202b738..5373414f 100644 --- a/tests/Unit/SearchableScopeTest.php +++ b/tests/Unit/SearchableScopeTest.php @@ -16,33 +16,34 @@ protected function tearDown(): void public function test_chunks_by_id() { - $builder = m::mock(Builder::class); + $builder = m::spy(Builder::class); + $builder->shouldReceive('macro')->with('searchable', m::on(function ($callback) use ($builder) { $model = m::mock(Model::class); $model->shouldReceive('getScoutKeyName')->once()->andReturn('id'); - $builder->shouldReceive('chunkById')->with(500, m::type(\Closure::class), 'users.id', 'id'); + $builder->shouldReceive('chunkById')->with(500, m::type(\Closure::class), 'users.id', 'id')->once(); $builder->shouldReceive('getModel')->once()->andReturn($model); $builder->shouldReceive('qualifyColumn')->once()->andReturn('users.id'); $callback($builder, 500); return true; - })); + }))->once(); $builder->shouldReceive('macro')->with('unsearchable', m::on(function ($callback) use ($builder) { $model = m::mock(Model::class); $model->shouldReceive('getScoutKeyName')->once()->andReturn('id'); - $builder->shouldReceive('chunkById')->with(500, m::type(\Closure::class), 'users.id', 'id'); + $builder->shouldReceive('chunkById')->with(500, m::type(\Closure::class), 'users.id', 'id')->once(); $builder->shouldReceive('getModel')->once()->andReturn($model); $builder->shouldReceive('qualifyColumn')->once()->andReturn('users.id'); $callback($builder, 500); return true; - })); + }))->once(); - (new SearchableScope())->extend($builder); + (new SearchableScope)->extend($builder); } } diff --git a/workbench/app/Models/Chirp.php b/workbench/app/Models/Chirp.php new file mode 100644 index 00000000..bc9bb00d --- /dev/null +++ b/workbench/app/Models/Chirp.php @@ -0,0 +1,47 @@ +getScoutKeyName()]; + } + + /** {@inheritDoc} */ + public function getScoutKey() + { + return $this->scout_id; + } + + /** {@inheritDoc} */ + public function getScoutKeyName() + { + return 'scout_id'; + } + + public function toSearchableArray() + { + if (isset($_ENV['chirp.toSearchableArray'])) { + return value($_ENV['chirp.toSearchableArray'], $this); + } + + return [ + 'content' => $this->content, + ]; + } +} diff --git a/workbench/app/Models/SearchableUser.php b/workbench/app/Models/SearchableUser.php new file mode 100644 index 00000000..a56899f8 --- /dev/null +++ b/workbench/app/Models/SearchableUser.php @@ -0,0 +1,64 @@ + $this->id, + 'name' => $this->name, + 'email' => $this->email, + ]; + } + + /** {@inheritDoc} */ + public function wasSearchableBeforeUpdate() + { + if (isset($_ENV['user.wasSearchableBeforeUpdate'])) { + return value($_ENV['user.wasSearchableBeforeUpdate'], $this); + } + + return true; + } + + /** {@inheritDoc} */ + public function wasSearchableBeforeDelete() + { + if (isset($_ENV['user.wasSearchableBeforeDelete'])) { + return value($_ENV['user.wasSearchableBeforeDelete'], $this); + } + + return true; + } + + /** {@inheritDoc} */ + public function shouldBeSearchable() + { + if (isset($_ENV['user.shouldBeSearchable'])) { + return value($_ENV['user.shouldBeSearchable'], $this); + } + + return true; + } + + /** {@inheritDoc} */ + public function searchIndexShouldBeUpdated() + { + if (isset($_ENV['user.searchIndexShouldBeUpdated'])) { + return value($_ENV['user.searchIndexShouldBeUpdated'], $this); + } + + return true; + } +} diff --git a/workbench/app/Models/User.php b/workbench/app/Models/User.php new file mode 100644 index 00000000..dc6027ef --- /dev/null +++ b/workbench/app/Models/User.php @@ -0,0 +1,46 @@ + + */ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'email_verified_at' => 'datetime', + // 'password' => 'hashed', + ]; +} diff --git a/workbench/app/Providers/WorkbenchServiceProvider.php b/workbench/app/Providers/WorkbenchServiceProvider.php new file mode 100644 index 00000000..7fcbd77a --- /dev/null +++ b/workbench/app/Providers/WorkbenchServiceProvider.php @@ -0,0 +1,35 @@ +app->singleton('scout.spied', function () { + return m::spy(NullEngine::class); + }); + + $this->callAfterResolving(EngineManager::class, function ($engine) { + $engine->extend('testing', function ($app) { + return $app->make('scout.spied'); + }); + }); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/workbench/database/factories/ChirpFactory.php b/workbench/database/factories/ChirpFactory.php new file mode 100644 index 00000000..6e54359d --- /dev/null +++ b/workbench/database/factories/ChirpFactory.php @@ -0,0 +1,34 @@ + + */ +class ChirpFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = Chirp::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'scout_id' => fake()->uuid(), + 'content' => fake()->realText(), + ]; + } +} diff --git a/workbench/database/factories/SearchableUserFactory.php b/workbench/database/factories/SearchableUserFactory.php new file mode 100644 index 00000000..4adccadb --- /dev/null +++ b/workbench/database/factories/SearchableUserFactory.php @@ -0,0 +1,15 @@ + + */ + protected $model = SearchableUser::class; +} diff --git a/workbench/database/factories/UserFactory.php b/workbench/database/factories/UserFactory.php new file mode 100644 index 00000000..dfcab01b --- /dev/null +++ b/workbench/database/factories/UserFactory.php @@ -0,0 +1,54 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = User::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/workbench/database/migrations/2024_11_12_030345_create_chirps_table.php b/workbench/database/migrations/2024_11_12_030345_create_chirps_table.php new file mode 100644 index 00000000..73a7fb61 --- /dev/null +++ b/workbench/database/migrations/2024_11_12_030345_create_chirps_table.php @@ -0,0 +1,30 @@ +id(); + $table->uuid('scout_id'); + $table->text('content'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('chirps'); + } +};