Skip to content
84 changes: 84 additions & 0 deletions tests/Feature/Actions/Scrape/FetchHtmlTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Actions\Scrape;

use App\Actions\Scrape\FetchHtml;
use App\Enums\Encoding;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Symfony\Component\DomCrawler\Crawler;
use Tests\Feature\TestCase;

final class FetchHtmlTest extends TestCase
{
public function test_invokes_http_and_returns_crawler(): void
{
$url = 'https://example.com/test';
$htmlContent = '<html><body><h1>Test Content</h1><script>alert(1);</script></body></html>';

Http::fake([
$url => Http::response($htmlContent, 200),
]);

$fetchHtml = new FetchHtml(
retryTimes: 1,
sleepMilliseconds: 10,
useCache: false
);

$crawler = $fetchHtml($url, Encoding::UTF_8);

$this->assertInstanceOf(Crawler::class, $crawler);
$this->assertStringContainsString('Test Content', $crawler->html());

Http::assertSentCount(1);
}

public function test_uses_cache_when_enabled(): void
{
$url = 'https://example.com/cached';
$cachedHtml = '<html><body>Cached Content</body></html>';

Cache::put('url:'.$url, $cachedHtml);

Http::fake();

$fetchHtml = new FetchHtml(
retryTimes: 1,
sleepMilliseconds: 10,
useCache: true
);

$crawler = $fetchHtml($url, Encoding::UTF_8);

$this->assertInstanceOf(Crawler::class, $crawler);
$this->assertStringContainsString('Cached Content', $crawler->html());

Http::assertNothingSent();
}

public function test_converts_encoding(): void
{
$url = 'https://example.com/euc-jp';
// HTML content encoded in EUC-JP
$eucJpHtml = mb_convert_encoding('<html><body>日本語</body></html>', 'EUC-JP', 'UTF-8');

Http::fake([
$url => Http::response($eucJpHtml, 200),
]);

$fetchHtml = new FetchHtml(
retryTimes: 1,
sleepMilliseconds: 10,
useCache: false
);

$crawler = $fetchHtml($url, Encoding::EUC_JP);

$this->assertInstanceOf(Crawler::class, $crawler);
// After conversion, it should be readable as UTF-8
$this->assertStringContainsString('日本語', $crawler->html());
}
}
54 changes: 54 additions & 0 deletions tests/Feature/Actions/Scrape/ScrapeActionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Actions\Scrape;

use App\Actions\Scrape\HandlerInterface;
use App\Actions\Scrape\Portal\Handler;
use App\Actions\Scrape\ScrapeAction;
use App\Enums\SiteName;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Tests\Feature\TestCase;

final class ScrapeActionTest extends TestCase
{
use RefreshDatabase;

public function test_invokes_handlers_for_all_sites_when_null_provided(): void
{
Http::fake([
'*' => Http::response('<html><body></body></html>', 200),
]);

// Mock PortalHandler to avoid database queries in CI where the portal connection is unmigrated
$this->app->bind(Handler::class, fn (): HandlerInterface => new class implements HandlerInterface
{
public function __invoke(LoggerInterface $logger): void {}
});

$scrapeAction = app(ScrapeAction::class);
$scrapeAction(null, new NullLogger);

// Verify JapanHandler was invoked by checking its specific HTTP request
Http::assertSent(fn ($request): bool => str_contains((string) $request->url(), 'japanese.simutrans.com'));
// Verify TwitransHandler was invoked
Http::assertSent(fn ($request): bool => str_contains((string) $request->url(), 'wikiwiki.jp/twitrans'));
}

public function test_invokes_specific_handler_when_site_provided(): void
{
Http::fake([
'*' => Http::response('<html><body></body></html>', 200),
]);

$scrapeAction = app(ScrapeAction::class);
$scrapeAction(SiteName::Japan, new NullLogger);

Http::assertSent(fn ($request): bool => str_contains((string) $request->url(), 'japanese.simutrans.com'));
Http::assertNotSent(fn ($request): bool => str_contains((string) $request->url(), 'wikiwiki.jp/twitrans'));
}
Comment on lines +9 to +53

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

The current tests use a dummy assertion ($this->assertTrue(true)) and only verify that no exceptions are thrown. This is an assertionless test pattern that does not actually verify the behavior of ScrapeAction (i.e., that it resolves the correct handlers from HandlerFactory and executes them).\n\nBy mocking HandlerFactory and HandlerInterface, we can assert that:\n1. The factory is called with the expected site names.\n2. The resolved handlers are actually invoked.\n\nThis also eliminates the need for Http::fake() in this test, making it a true unit/feature test for ScrapeAction in isolation.

use App\Actions\Scrape\HandlerFactory;\nuse App\Actions\Scrape\HandlerInterface;\nuse App\Actions\Scrape\ScrapeAction;\nuse App\Enums\SiteName;\nuse Psr\Log\NullLogger;\nuse Tests\Feature\TestCase;\n\nfinal class ScrapeActionTest extends TestCase\n{\n    public function test_invokes_handlers_for_all_sites_when_null_provided(): void\n    {\n        $handler = $this->mock(HandlerInterface::class, function ($mock) {\n            $mock->shouldReceive('__invoke')->once();\n        });\n\n        $this->mock(HandlerFactory::class, function ($mock) use ($handler) {\n            $mock->shouldReceive('create')\n                ->once()\n                ->with(SiteName::cases())\n                ->andReturn([$handler]);\n        });\n\n        $action = app(ScrapeAction::class);\n        $action(null, new NullLogger);\n    }\n\n    public function test_invokes_specific_handler_when_site_provided(): void\n    {\n        $handler = $this->mock(HandlerInterface::class, function ($mock) {\n            $mock->shouldReceive('__invoke')->once();\n        });\n\n        $this->mock(HandlerFactory::class, function ($mock) use ($handler) {\n            $mock->shouldReceive('create')\n                ->once()\n                ->with([SiteName::Japan])\n                ->andReturn([$handler]);\n        });\n\n        $action = app(ScrapeAction::class);\n        $action(SiteName::Japan, new NullLogger);\n    }

}
Loading