From c18cd3890c1958c6a9d5400bf9bbf3b9178f203d Mon Sep 17 00:00:00 2001 From: 128na Date: Tue, 2 Jun 2026 23:26:50 +0900 Subject: [PATCH 1/4] Feature: Add Tests for Search and SyncNotion Actions --- app/Actions/SearchPage/SearchAction.php | 12 +- .../Actions/SearchPage/SearchActionTest.php | 131 ++++++++++++++++++ .../Actions/SyncNotion/SyncActionTest.php | 90 ++++++++++++ 3 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 tests/Feature/Actions/SearchPage/SearchActionTest.php create mode 100644 tests/Feature/Actions/SyncNotion/SyncActionTest.php diff --git a/app/Actions/SearchPage/SearchAction.php b/app/Actions/SearchPage/SearchAction.php index 4f6d43d..fdad94e 100644 --- a/app/Actions/SearchPage/SearchAction.php +++ b/app/Actions/SearchPage/SearchAction.php @@ -39,12 +39,16 @@ private function addKeywordQuery(Builder $builder, string $keyword): void if (str_starts_with($word, '-')) { $word = trim(substr($word, 1)); if ($word !== '' && $word !== '0') { - $builder->where('title', 'not like', sprintf('%%%s%%', $word)); - $builder->orWhere('text', 'not like', sprintf('%%%s%%', $word)); + $builder->where(function (Builder $q) use ($word): void { + $q->where('title', 'not like', sprintf('%%%s%%', $word)); + $q->where('text', 'not like', sprintf('%%%s%%', $word)); + }); } } else { - $builder->where('title', 'like', sprintf('%%%s%%', $word)); - $builder->orWhere('text', 'like', sprintf('%%%s%%', $word)); + $builder->where(function (Builder $q) use ($word): void { + $q->where('title', 'like', sprintf('%%%s%%', $word)); + $q->orWhere('text', 'like', sprintf('%%%s%%', $word)); + }); } } }); diff --git a/tests/Feature/Actions/SearchPage/SearchActionTest.php b/tests/Feature/Actions/SearchPage/SearchActionTest.php new file mode 100644 index 0000000..733d3d2 --- /dev/null +++ b/tests/Feature/Actions/SearchPage/SearchActionTest.php @@ -0,0 +1,131 @@ +pak128 = Pak::factory()->create(['slug' => PakSlug::Pak128]); + $this->pak64 = Pak::factory()->create(['slug' => PakSlug::Pak64]); + } + + public function test_filters_by_site_and_pak(): void + { + $page1 = Page::factory()->create(['site_name' => SiteName::Japan]); + $page1->paks()->attach($this->pak128); + + $page2 = Page::factory()->create(['site_name' => SiteName::Portal]); + $page2->paks()->attach($this->pak128); + + $page3 = Page::factory()->create(['site_name' => SiteName::Japan]); + $page3->paks()->attach($this->pak64); + + $action = new SearchAction; + + $result = $action([ + 'keyword' => '', + 'sites' => [SiteName::Japan->value], + 'paks' => [PakSlug::Pak128->value], + ]); + + $this->assertCount(1, $result); + $this->assertSame($page1->id, $result->first()->id); + } + + public function test_filters_by_keyword_including_exclude_logic(): void + { + $page1 = Page::factory()->create([ + 'site_name' => SiteName::Japan, + 'title' => 'Train Addon', + 'text' => 'This is a train', + ]); + $page1->paks()->attach($this->pak128); + + $page2 = Page::factory()->create([ + 'site_name' => SiteName::Japan, + 'title' => 'Bus Addon', + 'text' => 'This is a bus', + ]); + $page2->paks()->attach($this->pak128); + + $page3 = Page::factory()->create([ + 'site_name' => SiteName::Japan, + 'title' => 'Electric Train Addon', + 'text' => 'This is an electric train', + ]); + $page3->paks()->attach($this->pak128); + + $action = new SearchAction; + + $baseQuery = [ + 'sites' => [SiteName::Japan->value], + 'paks' => [PakSlug::Pak128->value], + ]; + + // Search for 'Train' + $result = $action(array_merge($baseQuery, ['keyword' => 'Train'])); + $this->assertCount(2, $result); // page1, page3 + + // Search for 'Train -Electric' + $result = $action(array_merge($baseQuery, ['keyword' => 'Train -Electric'])); + $this->assertCount(1, $result); // page1 only + + // Search for 'Bus' + $result = $action(array_merge($baseQuery, ['keyword' => 'Bus'])); + $this->assertCount(1, $result); // page2 + $this->assertSame($page2->id, $result->first()->id); + } + + public function test_orders_by_last_modified_desc(): void + { + $page1 = Page::factory()->create([ + 'site_name' => SiteName::Japan, + 'last_modified' => now()->subDays(2), + ]); + $page1->paks()->attach($this->pak128); + + $page2 = Page::factory()->create([ + 'site_name' => SiteName::Japan, + 'last_modified' => now(), + ]); + $page2->paks()->attach($this->pak128); + + $page3 = Page::factory()->create([ + 'site_name' => SiteName::Japan, + 'last_modified' => now()->subDays(1), + ]); + $page3->paks()->attach($this->pak128); + + $action = new SearchAction; + + $result = $action([ + 'keyword' => '', + 'sites' => [SiteName::Japan->value], + 'paks' => [PakSlug::Pak128->value], + ]); + + $this->assertCount(3, $result); + $this->assertSame($page2->id, $result->items()[0]->id); + $this->assertSame($page3->id, $result->items()[1]->id); + $this->assertSame($page1->id, $result->items()[2]->id); + } +} diff --git a/tests/Feature/Actions/SyncNotion/SyncActionTest.php b/tests/Feature/Actions/SyncNotion/SyncActionTest.php new file mode 100644 index 0000000..3690b3c --- /dev/null +++ b/tests/Feature/Actions/SyncNotion/SyncActionTest.php @@ -0,0 +1,90 @@ +create(['slug' => PakSlug::Pak128]); + + // This page should be created in Notion + $pageToCreate = Page::factory()->create([ + 'title' => 'New Addon', + 'site_name' => SiteName::Japan, + 'url' => 'https://example.com/new', + 'last_modified' => now(), + ]); + $pageToCreate->paks()->attach($pak); + + // This page exists in both DB and Notion, should be updated + $pageToUpdate = Page::factory()->create([ + 'title' => 'Updated Addon', + 'site_name' => SiteName::Japan, + 'url' => 'https://example.com/update', + 'last_modified' => now(), + ]); + + $mockDatabase = Mockery::mock(Database::class); + $this->setReadonlyProperty($mockDatabase, 'id', 'test_database_id'); + + $multiSelect = MultiSelect::create('パックセット', [ + SelectOption::fromName('128'), + ]); + $propertyCollection = (new \ReflectionClass(PropertyCollection::class))->newInstanceWithoutConstructor(); + $this->setReadonlyProperty($propertyCollection, 'properties', ['パックセット' => $multiSelect]); + + $mockDatabase->shouldReceive('properties')->andReturn($propertyCollection); + + $notionPageToUpdate = NotionPage::create(PageParent::database('test_database_id')); + $notionPageToUpdate = $notionPageToUpdate->addProperty('URL', Url::create('https://example.com/update')); + + $notionPageToDelete = NotionPage::create(PageParent::database('test_database_id')); + $notionPageToDelete = $notionPageToDelete->addProperty('URL', Url::create('https://example.com/delete')); + + $notion = Mockery::mock(Notion::class); + $notion->shouldReceive('databases->find')->with('test_database_id')->andReturn($mockDatabase); + $notion->shouldReceive('databases->queryAllPages')->with($mockDatabase)->andReturn([$notionPageToUpdate, $notionPageToDelete]); + + $notion->shouldReceive('pages->delete')->once()->with($notionPageToDelete); + + $notion->shouldReceive('pages->update')->once()->with(Mockery::on(function (NotionPage $page) { + return $page->getProperty('URL')->url === 'https://example.com/update'; + })); + + $notion->shouldReceive('pages->create')->once()->with(Mockery::on(function (NotionPage $page) { + return $page->getProperty('URL')->url === 'https://example.com/new'; + })); + + $action = new SyncAction($notion); + $action('test_database_id', 10); + } + + private function setReadonlyProperty(object $object, string $property, mixed $value): void + { + $reflection = new \ReflectionProperty($object, $property); + $reflection->setAccessible(true); + $reflection->setValue($object, $value); + } +} From 395691a52eec534f7d9c31e4670d00e08aacb67f Mon Sep 17 00:00:00 2001 From: 128na Date: Tue, 2 Jun 2026 23:35:45 +0900 Subject: [PATCH 2/4] Fix: Address code review feedback (Skip empty keyword, remove reflection in tests) --- app/Actions/SearchPage/SearchAction.php | 4 ++ .../Actions/SyncNotion/SyncActionTest.php | 52 +++++++++++-------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/app/Actions/SearchPage/SearchAction.php b/app/Actions/SearchPage/SearchAction.php index fdad94e..f799b46 100644 --- a/app/Actions/SearchPage/SearchAction.php +++ b/app/Actions/SearchPage/SearchAction.php @@ -36,6 +36,10 @@ private function addKeywordQuery(Builder $builder, string $keyword): void $builder->where(function (Builder $builder) use ($keyword): void { foreach (explode(' ', $keyword) as $word) { $word = trim($word); + if ($word === '') { + continue; + } + if (str_starts_with($word, '-')) { $word = trim(substr($word, 1)); if ($word !== '' && $word !== '0') { diff --git a/tests/Feature/Actions/SyncNotion/SyncActionTest.php b/tests/Feature/Actions/SyncNotion/SyncActionTest.php index 3690b3c..628be3f 100644 --- a/tests/Feature/Actions/SyncNotion/SyncActionTest.php +++ b/tests/Feature/Actions/SyncNotion/SyncActionTest.php @@ -12,9 +12,6 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; use Notion\Databases\Database; -use Notion\Databases\Properties\MultiSelect; -use Notion\Databases\Properties\PropertyCollection; -use Notion\Databases\Properties\SelectOption; use Notion\Notion; use Notion\Pages\Page as NotionPage; use Notion\Pages\PageParent; @@ -46,16 +43,36 @@ public function test_sync_action_creates_updates_and_deletes_pages(): void 'last_modified' => now(), ]); - $mockDatabase = Mockery::mock(Database::class); - $this->setReadonlyProperty($mockDatabase, 'id', 'test_database_id'); - - $multiSelect = MultiSelect::create('パックセット', [ - SelectOption::fromName('128'), + $database = Database::fromArray([ + 'id' => 'test_database_id', + 'created_time' => '2023-01-01T00:00:00.000Z', + 'last_edited_time' => '2023-01-01T00:00:00.000Z', + 'title' => [], + 'description' => [], + 'icon' => null, + 'cover' => null, + 'properties' => [ + 'Title' => [ + 'id' => 'title', + 'type' => 'title', + 'name' => 'Title', + 'title' => [], + ], + 'パックセット' => [ + 'id' => 'prop_id', + 'type' => 'multi_select', + 'name' => 'パックセット', + 'multi_select' => [ + 'options' => [ + ['name' => '128', 'id' => 'opt_128', 'color' => 'default'], + ], + ], + ], + ], + 'parent' => ['type' => 'workspace', 'workspace' => true], + 'url' => 'https://notion.so', + 'is_inline' => false, ]); - $propertyCollection = (new \ReflectionClass(PropertyCollection::class))->newInstanceWithoutConstructor(); - $this->setReadonlyProperty($propertyCollection, 'properties', ['パックセット' => $multiSelect]); - - $mockDatabase->shouldReceive('properties')->andReturn($propertyCollection); $notionPageToUpdate = NotionPage::create(PageParent::database('test_database_id')); $notionPageToUpdate = $notionPageToUpdate->addProperty('URL', Url::create('https://example.com/update')); @@ -64,8 +81,8 @@ public function test_sync_action_creates_updates_and_deletes_pages(): void $notionPageToDelete = $notionPageToDelete->addProperty('URL', Url::create('https://example.com/delete')); $notion = Mockery::mock(Notion::class); - $notion->shouldReceive('databases->find')->with('test_database_id')->andReturn($mockDatabase); - $notion->shouldReceive('databases->queryAllPages')->with($mockDatabase)->andReturn([$notionPageToUpdate, $notionPageToDelete]); + $notion->shouldReceive('databases->find')->with('test_database_id')->andReturn($database); + $notion->shouldReceive('databases->queryAllPages')->with($database)->andReturn([$notionPageToUpdate, $notionPageToDelete]); $notion->shouldReceive('pages->delete')->once()->with($notionPageToDelete); @@ -80,11 +97,4 @@ public function test_sync_action_creates_updates_and_deletes_pages(): void $action = new SyncAction($notion); $action('test_database_id', 10); } - - private function setReadonlyProperty(object $object, string $property, mixed $value): void - { - $reflection = new \ReflectionProperty($object, $property); - $reflection->setAccessible(true); - $reflection->setValue($object, $value); - } } From 95d04f9974c7db511ddade9c2f6d3355e0ce1235 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 2 Jun 2026 14:37:29 +0000 Subject: [PATCH 3/4] [rector] Rector fixes --- app/Actions/SearchPage/SearchAction.php | 12 +++++----- .../Actions/SearchPage/SearchActionTest.php | 16 +++++++------- .../Actions/SyncNotion/SyncActionTest.php | 22 ++++++++----------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/app/Actions/SearchPage/SearchAction.php b/app/Actions/SearchPage/SearchAction.php index f799b46..ffcac0c 100644 --- a/app/Actions/SearchPage/SearchAction.php +++ b/app/Actions/SearchPage/SearchAction.php @@ -43,15 +43,15 @@ private function addKeywordQuery(Builder $builder, string $keyword): void if (str_starts_with($word, '-')) { $word = trim(substr($word, 1)); if ($word !== '' && $word !== '0') { - $builder->where(function (Builder $q) use ($word): void { - $q->where('title', 'not like', sprintf('%%%s%%', $word)); - $q->where('text', 'not like', sprintf('%%%s%%', $word)); + $builder->where(function (Builder $builder) use ($word): void { + $builder->where('title', 'not like', sprintf('%%%s%%', $word)); + $builder->where('text', 'not like', sprintf('%%%s%%', $word)); }); } } else { - $builder->where(function (Builder $q) use ($word): void { - $q->where('title', 'like', sprintf('%%%s%%', $word)); - $q->orWhere('text', 'like', sprintf('%%%s%%', $word)); + $builder->where(function (Builder $builder) use ($word): void { + $builder->where('title', 'like', sprintf('%%%s%%', $word)); + $builder->orWhere('text', 'like', sprintf('%%%s%%', $word)); }); } } diff --git a/tests/Feature/Actions/SearchPage/SearchActionTest.php b/tests/Feature/Actions/SearchPage/SearchActionTest.php index 733d3d2..d61badf 100644 --- a/tests/Feature/Actions/SearchPage/SearchActionTest.php +++ b/tests/Feature/Actions/SearchPage/SearchActionTest.php @@ -39,9 +39,9 @@ public function test_filters_by_site_and_pak(): void $page3 = Page::factory()->create(['site_name' => SiteName::Japan]); $page3->paks()->attach($this->pak64); - $action = new SearchAction; + $searchAction = new SearchAction; - $result = $action([ + $result = $searchAction([ 'keyword' => '', 'sites' => [SiteName::Japan->value], 'paks' => [PakSlug::Pak128->value], @@ -74,7 +74,7 @@ public function test_filters_by_keyword_including_exclude_logic(): void ]); $page3->paks()->attach($this->pak128); - $action = new SearchAction; + $searchAction = new SearchAction; $baseQuery = [ 'sites' => [SiteName::Japan->value], @@ -82,15 +82,15 @@ public function test_filters_by_keyword_including_exclude_logic(): void ]; // Search for 'Train' - $result = $action(array_merge($baseQuery, ['keyword' => 'Train'])); + $result = $searchAction(array_merge($baseQuery, ['keyword' => 'Train'])); $this->assertCount(2, $result); // page1, page3 // Search for 'Train -Electric' - $result = $action(array_merge($baseQuery, ['keyword' => 'Train -Electric'])); + $result = $searchAction(array_merge($baseQuery, ['keyword' => 'Train -Electric'])); $this->assertCount(1, $result); // page1 only // Search for 'Bus' - $result = $action(array_merge($baseQuery, ['keyword' => 'Bus'])); + $result = $searchAction(array_merge($baseQuery, ['keyword' => 'Bus'])); $this->assertCount(1, $result); // page2 $this->assertSame($page2->id, $result->first()->id); } @@ -115,9 +115,9 @@ public function test_orders_by_last_modified_desc(): void ]); $page3->paks()->attach($this->pak128); - $action = new SearchAction; + $searchAction = new SearchAction; - $result = $action([ + $result = $searchAction([ 'keyword' => '', 'sites' => [SiteName::Japan->value], 'paks' => [PakSlug::Pak128->value], diff --git a/tests/Feature/Actions/SyncNotion/SyncActionTest.php b/tests/Feature/Actions/SyncNotion/SyncActionTest.php index 628be3f..908792d 100644 --- a/tests/Feature/Actions/SyncNotion/SyncActionTest.php +++ b/tests/Feature/Actions/SyncNotion/SyncActionTest.php @@ -36,7 +36,7 @@ public function test_sync_action_creates_updates_and_deletes_pages(): void $pageToCreate->paks()->attach($pak); // This page exists in both DB and Notion, should be updated - $pageToUpdate = Page::factory()->create([ + Page::factory()->create([ 'title' => 'Updated Addon', 'site_name' => SiteName::Japan, 'url' => 'https://example.com/update', @@ -80,21 +80,17 @@ public function test_sync_action_creates_updates_and_deletes_pages(): void $notionPageToDelete = NotionPage::create(PageParent::database('test_database_id')); $notionPageToDelete = $notionPageToDelete->addProperty('URL', Url::create('https://example.com/delete')); - $notion = Mockery::mock(Notion::class); - $notion->shouldReceive('databases->find')->with('test_database_id')->andReturn($database); - $notion->shouldReceive('databases->queryAllPages')->with($database)->andReturn([$notionPageToUpdate, $notionPageToDelete]); + $mock = Mockery::mock(Notion::class); + $mock->shouldReceive('databases->find')->with('test_database_id')->andReturn($database); + $mock->shouldReceive('databases->queryAllPages')->with($database)->andReturn([$notionPageToUpdate, $notionPageToDelete]); - $notion->shouldReceive('pages->delete')->once()->with($notionPageToDelete); + $mock->shouldReceive('pages->delete')->once()->with($notionPageToDelete); - $notion->shouldReceive('pages->update')->once()->with(Mockery::on(function (NotionPage $page) { - return $page->getProperty('URL')->url === 'https://example.com/update'; - })); + $mock->shouldReceive('pages->update')->once()->with(Mockery::on(fn (NotionPage $page) => $page->getProperty('URL')->url === 'https://example.com/update')); - $notion->shouldReceive('pages->create')->once()->with(Mockery::on(function (NotionPage $page) { - return $page->getProperty('URL')->url === 'https://example.com/new'; - })); + $mock->shouldReceive('pages->create')->once()->with(Mockery::on(fn (NotionPage $page) => $page->getProperty('URL')->url === 'https://example.com/new')); - $action = new SyncAction($notion); - $action('test_database_id', 10); + $syncAction = new SyncAction($mock); + $syncAction('test_database_id', 10); } } From 4321fe9cdc7946bb00fe6c2fb1c158d6359762c5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 2 Jun 2026 14:38:05 +0000 Subject: [PATCH 4/4] [rector] Rector fixes --- tests/Feature/Actions/SyncNotion/SyncActionTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Actions/SyncNotion/SyncActionTest.php b/tests/Feature/Actions/SyncNotion/SyncActionTest.php index 908792d..1604f57 100644 --- a/tests/Feature/Actions/SyncNotion/SyncActionTest.php +++ b/tests/Feature/Actions/SyncNotion/SyncActionTest.php @@ -86,9 +86,9 @@ public function test_sync_action_creates_updates_and_deletes_pages(): void $mock->shouldReceive('pages->delete')->once()->with($notionPageToDelete); - $mock->shouldReceive('pages->update')->once()->with(Mockery::on(fn (NotionPage $page) => $page->getProperty('URL')->url === 'https://example.com/update')); + $mock->shouldReceive('pages->update')->once()->with(Mockery::on(fn (NotionPage $notionPage): bool => $notionPage->getProperty('URL')->url === 'https://example.com/update')); - $mock->shouldReceive('pages->create')->once()->with(Mockery::on(fn (NotionPage $page) => $page->getProperty('URL')->url === 'https://example.com/new')); + $mock->shouldReceive('pages->create')->once()->with(Mockery::on(fn (NotionPage $notionPage): bool => $notionPage->getProperty('URL')->url === 'https://example.com/new')); $syncAction = new SyncAction($mock); $syncAction('test_database_id', 10);