Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions app/Actions/SearchPage/SearchAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,23 @@ 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') {
Comment on lines 43 to 45
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

キーワードに複数の連続したスペースが含まれている場合、explode(' ', $keyword) によって空文字列 "" が生成されます。このとき、str_starts_with($word, '-')false となり、else ブロックが実行されて LIKE '%%' という不要なクエリが追加されてしまいます。

これを防ぐために、ループの先頭で空文字列をスキップすることをお勧めします。

また、日本語環境では全角スペース( )が区切り文字として使われることが多いため、将来的に全角スペースへの対応(preg_split への変更など)も検討するとより良くなります。

                if ($word === '') {
                    continue;
                }
                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 $builder) use ($word): void {
$builder->where('title', 'not like', sprintf('%%%s%%', $word));
$builder->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 $builder) use ($word): void {
$builder->where('title', 'like', sprintf('%%%s%%', $word));
$builder->orWhere('text', 'like', sprintf('%%%s%%', $word));
});
}
}
});
Expand Down
131 changes: 131 additions & 0 deletions tests/Feature/Actions/SearchPage/SearchActionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Actions\SearchPage;

use App\Actions\SearchPage\SearchAction;
use App\Enums\PakSlug;
use App\Enums\SiteName;
use App\Models\Page;
use App\Models\Pak;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Feature\TestCase;

final class SearchActionTest extends TestCase
{
use RefreshDatabase;

private Pak $pak128;

private Pak $pak64;

protected function setUp(): void
{
parent::setUp();

$this->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);

$searchAction = new SearchAction;

$result = $searchAction([
'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);

$searchAction = new SearchAction;

$baseQuery = [
'sites' => [SiteName::Japan->value],
'paks' => [PakSlug::Pak128->value],
];

// Search for 'Train'
$result = $searchAction(array_merge($baseQuery, ['keyword' => 'Train']));
$this->assertCount(2, $result); // page1, page3

// Search for 'Train -Electric'
$result = $searchAction(array_merge($baseQuery, ['keyword' => 'Train -Electric']));
$this->assertCount(1, $result); // page1 only

// Search for 'Bus'
$result = $searchAction(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);

$searchAction = new SearchAction;

$result = $searchAction([
'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);
}
}
96 changes: 96 additions & 0 deletions tests/Feature/Actions/SyncNotion/SyncActionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Actions\SyncNotion;

use App\Actions\SyncNotion\SyncAction;
use App\Enums\PakSlug;
use App\Enums\SiteName;
use App\Models\Page;
use App\Models\Pak;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Notion\Databases\Database;
use Notion\Notion;
use Notion\Pages\Page as NotionPage;
use Notion\Pages\PageParent;
use Notion\Pages\Properties\Url;
use Tests\Feature\TestCase;

final class SyncActionTest extends TestCase
{
use RefreshDatabase;

public function test_sync_action_creates_updates_and_deletes_pages(): void
{
$pak = Pak::factory()->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
Page::factory()->create([
'title' => 'Updated Addon',
'site_name' => SiteName::Japan,
'url' => 'https://example.com/update',
'last_modified' => now(),
]);

$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,
]);

$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'));

$mock = Mockery::mock(Notion::class);
$mock->shouldReceive('databases->find')->with('test_database_id')->andReturn($database);
$mock->shouldReceive('databases->queryAllPages')->with($database)->andReturn([$notionPageToUpdate, $notionPageToDelete]);

$mock->shouldReceive('pages->delete')->once()->with($notionPageToDelete);

$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 $notionPage): bool => $notionPage->getProperty('URL')->url === 'https://example.com/new'));

$syncAction = new SyncAction($mock);
$syncAction('test_database_id', 10);
}
}
Loading