Compare commits
3 commits
19cbea9273
...
ec09711a6f
| Author | SHA1 | Date | |
|---|---|---|---|
| ec09711a6f | |||
| bf96489362 | |||
| 0bb10729de |
21 changed files with 281 additions and 108 deletions
|
|
@ -5,4 +5,18 @@
|
||||||
enum PlatformEnum: string
|
enum PlatformEnum: string
|
||||||
{
|
{
|
||||||
case LEMMY = 'lemmy';
|
case LEMMY = 'lemmy';
|
||||||
|
|
||||||
|
public function channelLabel(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::LEMMY => 'Community',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function channelLabelPlural(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::LEMMY => 'Communities',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
app/Events/ActionPerformed.php
Normal file
18
app/Events/ActionPerformed.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Enums\LogLevelEnum;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
|
||||||
|
class ActionPerformed
|
||||||
|
{
|
||||||
|
use Dispatchable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public string $message,
|
||||||
|
public LogLevelEnum $level = LogLevelEnum::INFO,
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
public array $context = [],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Jobs;
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Enums\LogLevelEnum;
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
use App\Exceptions\PublishException;
|
use App\Exceptions\PublishException;
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use App\Models\ArticlePublication;
|
use App\Models\ArticlePublication;
|
||||||
|
|
@ -53,7 +55,7 @@ public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger()->info('Publishing next article from scheduled job', [
|
ActionPerformed::dispatch('Publishing next article from scheduled job', LogLevelEnum::INFO, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'title' => $article->title,
|
'title' => $article->title,
|
||||||
'url' => $article->url,
|
'url' => $article->url,
|
||||||
|
|
@ -66,12 +68,12 @@ public function handle(ArticleFetcher $articleFetcher, ArticlePublishingService
|
||||||
try {
|
try {
|
||||||
$publishingService->publishToRoutedChannels($article, $extractedData);
|
$publishingService->publishToRoutedChannels($article, $extractedData);
|
||||||
|
|
||||||
logger()->info('Successfully published article', [
|
ActionPerformed::dispatch('Successfully published article', LogLevelEnum::INFO, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'title' => $article->title,
|
'title' => $article->title,
|
||||||
]);
|
]);
|
||||||
} catch (PublishException $e) {
|
} catch (PublishException $e) {
|
||||||
logger()->error('Failed to publish article', [
|
ActionPerformed::dispatch('Failed to publish article', LogLevelEnum::ERROR, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
21
app/Listeners/LogActionListener.php
Normal file
21
app/Listeners/LogActionListener.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
|
use App\Services\Log\LogSaver;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class LogActionListener
|
||||||
|
{
|
||||||
|
public function __construct(private LogSaver $logSaver) {}
|
||||||
|
|
||||||
|
public function handle(ActionPerformed $event): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->logSaver->log($event->level, $event->message, context: $event->context);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log('Failed to log action to database: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Listeners;
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Enums\LogLevelEnum;
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
use App\Events\ArticleApproved;
|
use App\Events\ArticleApproved;
|
||||||
use App\Services\Article\ArticleFetcher;
|
use App\Services\Article\ArticleFetcher;
|
||||||
use App\Services\Publishing\ArticlePublishingService;
|
use App\Services\Publishing\ArticlePublishingService;
|
||||||
|
|
@ -40,14 +42,14 @@ public function handle(ArticleApproved $event): void
|
||||||
if ($publications->isNotEmpty()) {
|
if ($publications->isNotEmpty()) {
|
||||||
$article->update(['publish_status' => 'published']);
|
$article->update(['publish_status' => 'published']);
|
||||||
|
|
||||||
logger()->info('Published approved article', [
|
ActionPerformed::dispatch('Published approved article', LogLevelEnum::INFO, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'title' => $article->title,
|
'title' => $article->title,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$article->update(['publish_status' => 'error']);
|
$article->update(['publish_status' => 'error']);
|
||||||
|
|
||||||
logger()->warning('No publications created for approved article', [
|
ActionPerformed::dispatch('No publications created for approved article', LogLevelEnum::WARNING, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'title' => $article->title,
|
'title' => $article->title,
|
||||||
]);
|
]);
|
||||||
|
|
@ -55,7 +57,7 @@ public function handle(ArticleApproved $event): void
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$article->update(['publish_status' => 'error']);
|
$article->update(['publish_status' => 'error']);
|
||||||
|
|
||||||
logger()->error('Failed to publish approved article', [
|
ActionPerformed::dispatch('Failed to publish approved article', LogLevelEnum::ERROR, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Listeners;
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Enums\LogLevelEnum;
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
use App\Events\NewArticleFetched;
|
use App\Events\NewArticleFetched;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Services\Article\ValidationService;
|
use App\Services\Article\ValidationService;
|
||||||
|
|
@ -37,7 +39,7 @@ public function handle(NewArticleFetched $event): void
|
||||||
try {
|
try {
|
||||||
$article = $this->validationService->validate($article);
|
$article = $this->validationService->validate($article);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
logger()->error('Article validation failed', [
|
ActionPerformed::dispatch('Article validation failed', LogLevelEnum::ERROR, [
|
||||||
'article_id' => $article->id,
|
'article_id' => $article->id,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ public function deleteKeyword(int $keywordId): void
|
||||||
|
|
||||||
public function render(): \Illuminate\Contracts\View\View
|
public function render(): \Illuminate\Contracts\View\View
|
||||||
{
|
{
|
||||||
$routes = Route::with(['feed', 'platformChannel'])
|
$routes = Route::with(['feed', 'platformChannel.platformInstance'])
|
||||||
->orderBy('priority', 'desc')
|
->orderBy('priority', 'desc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
|
@ -186,7 +186,7 @@ public function render(): \Illuminate\Contracts\View\View
|
||||||
$editingKeywords = collect();
|
$editingKeywords = collect();
|
||||||
|
|
||||||
if ($this->editingFeedId && $this->editingChannelId) {
|
if ($this->editingFeedId && $this->editingChannelId) {
|
||||||
$editingRoute = Route::with(['feed', 'platformChannel'])
|
$editingRoute = Route::with(['feed', 'platformChannel.platformInstance'])
|
||||||
->where('feed_id', $this->editingFeedId)
|
->where('feed_id', $this->editingFeedId)
|
||||||
->where('platform_channel_id', $this->editingChannelId)
|
->where('platform_channel_id', $this->editingChannelId)
|
||||||
->first();
|
->first();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Enums\LogLevelEnum;
|
use App\Enums\LogLevelEnum;
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
use App\Events\ExceptionOccurred;
|
use App\Events\ExceptionOccurred;
|
||||||
|
use App\Listeners\LogActionListener;
|
||||||
use App\Listeners\LogExceptionToDatabase;
|
use App\Listeners\LogExceptionToDatabase;
|
||||||
use Error;
|
use Error;
|
||||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||||
|
|
@ -18,6 +20,11 @@ public function register(): void {}
|
||||||
|
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
Event::listen(
|
||||||
|
ActionPerformed::class,
|
||||||
|
LogActionListener::class,
|
||||||
|
);
|
||||||
|
|
||||||
Event::listen(
|
Event::listen(
|
||||||
ExceptionOccurred::class,
|
ExceptionOccurred::class,
|
||||||
LogExceptionToDatabase::class,
|
LogExceptionToDatabase::class,
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ public function debug(string $message, ?PlatformChannel $channel = null, array $
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $context
|
* @param array<string, mixed> $context
|
||||||
*/
|
*/
|
||||||
private function log(LogLevelEnum $level, string $message, ?PlatformChannel $channel = null, array $context = []): void
|
public function log(LogLevelEnum $level, string $message, ?PlatformChannel $channel = null, array $context = []): void
|
||||||
{
|
{
|
||||||
$logContext = $context;
|
$logContext = $context;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services\Parsers;
|
|
||||||
|
|
||||||
class BelgaHomepageParser
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
public static function extractArticleUrls(string $html): array
|
|
||||||
{
|
|
||||||
// Find all relative article links (most articles use relative paths)
|
|
||||||
preg_match_all('/<a[^>]+href="(\/[a-z0-9-]+)"/', $html, $matches);
|
|
||||||
|
|
||||||
// Blacklist of non-article paths
|
|
||||||
$blacklistPaths = [
|
|
||||||
'/',
|
|
||||||
'/de',
|
|
||||||
'/feed',
|
|
||||||
'/search',
|
|
||||||
'/category',
|
|
||||||
'/about',
|
|
||||||
'/contact',
|
|
||||||
'/privacy',
|
|
||||||
'/terms',
|
|
||||||
];
|
|
||||||
|
|
||||||
$urls = collect($matches[1])
|
|
||||||
->unique()
|
|
||||||
->filter(function ($path) use ($blacklistPaths) {
|
|
||||||
// Exclude exact matches and paths starting with blacklisted paths
|
|
||||||
foreach ($blacklistPaths as $blacklistedPath) {
|
|
||||||
if ($path === $blacklistedPath || str_starts_with($path, $blacklistedPath.'/')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
->map(function ($path) {
|
|
||||||
// Convert relative paths to absolute URLs
|
|
||||||
return 'https://www.belganewsagency.eu'.$path;
|
|
||||||
})
|
|
||||||
->values()
|
|
||||||
->toArray();
|
|
||||||
|
|
||||||
return $urls;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services\Parsers;
|
|
||||||
|
|
||||||
use App\Contracts\HomepageParserInterface;
|
|
||||||
|
|
||||||
class BelgaHomepageParserAdapter implements HomepageParserInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly string $language = 'en',
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function getLanguage(): string
|
|
||||||
{
|
|
||||||
return $this->language;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function canParse(string $url): bool
|
|
||||||
{
|
|
||||||
return str_contains($url, 'belganewsagency.eu');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function extractArticleUrls(string $html): array
|
|
||||||
{
|
|
||||||
return BelgaHomepageParser::extractArticleUrls($html);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getHomepageUrl(): string
|
|
||||||
{
|
|
||||||
return 'https://www.belganewsagency.eu/';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSourceName(): string
|
|
||||||
{
|
|
||||||
return 'Belga News Agency';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -33,13 +33,12 @@
|
||||||
'code' => 'belga',
|
'code' => 'belga',
|
||||||
'name' => 'Belga News Agency',
|
'name' => 'Belga News Agency',
|
||||||
'description' => 'Belgian national news agency',
|
'description' => 'Belgian national news agency',
|
||||||
'type' => 'website',
|
'type' => 'rss',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'languages' => [
|
'languages' => [
|
||||||
'en' => ['url' => 'https://www.belganewsagency.eu/'],
|
'en' => ['url' => 'https://www.belganewsagency.eu/feed'],
|
||||||
],
|
],
|
||||||
'parsers' => [
|
'parsers' => [
|
||||||
'homepage' => \App\Services\Parsers\BelgaHomepageParserAdapter::class,
|
|
||||||
'article' => \App\Services\Parsers\BelgaArticleParser::class,
|
'article' => \App\Services\Parsers\BelgaArticleParser::class,
|
||||||
'article_page' => \App\Services\Parsers\BelgaArticlePageParser::class,
|
'article_page' => \App\Services\Parsers\BelgaArticlePageParser::class,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@ public function belga(): static
|
||||||
{
|
{
|
||||||
return $this->state(fn (array $attributes) => [
|
return $this->state(fn (array $attributes) => [
|
||||||
'provider' => 'belga',
|
'provider' => 'belga',
|
||||||
'url' => 'https://www.belganewsagency.eu/',
|
'url' => 'https://www.belganewsagency.eu/feed',
|
||||||
|
'type' => 'rss',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>Feed: {{ $route->feed?->name }}</span>
|
<span>Feed: {{ $route->feed?->name }}</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>Channel: {{ $route->platformChannel?->display_name ?? $route->platformChannel?->name }}</span>
|
<span>{{ $route->platformChannel?->platformInstance?->platform?->channelLabel() ?? 'Channel' }}: {{ $route->platformChannel?->display_name ?? $route->platformChannel?->name }}</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>Created: {{ $route->created_at->format('M d, Y') }}</span>
|
<span>Created: {{ $route->created_at->format('M d, Y') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -246,7 +246,7 @@ class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transp
|
||||||
<strong>Feed:</strong> {{ $editingRoute->feed?->name }}
|
<strong>Feed:</strong> {{ $editingRoute->feed?->name }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-gray-600">
|
<p class="text-sm text-gray-600">
|
||||||
<strong>Channel:</strong> {{ $editingRoute->platformChannel?->display_name ?? $editingRoute->platformChannel?->name }}
|
<strong>{{ $editingRoute->platformChannel?->platformInstance?->platform?->channelLabel() ?? 'Channel' }}:</strong> {{ $editingRoute->platformChannel?->display_name ?? $editingRoute->platformChannel?->name }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,16 +98,16 @@ public function test_store_creates_belga_feed_successfully(): void
|
||||||
'message' => 'Feed created successfully!',
|
'message' => 'Feed created successfully!',
|
||||||
'data' => [
|
'data' => [
|
||||||
'name' => 'Belga Test Feed',
|
'name' => 'Belga Test Feed',
|
||||||
'url' => 'https://www.belganewsagency.eu/',
|
'url' => 'https://www.belganewsagency.eu/feed',
|
||||||
'type' => 'website',
|
'type' => 'rss',
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('feeds', [
|
$this->assertDatabaseHas('feeds', [
|
||||||
'name' => 'Belga Test Feed',
|
'name' => 'Belga Test Feed',
|
||||||
'url' => 'https://www.belganewsagency.eu/',
|
'url' => 'https://www.belganewsagency.eu/feed',
|
||||||
'type' => 'website',
|
'type' => 'rss',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
use App\Events\ArticleApproved;
|
use App\Events\ArticleApproved;
|
||||||
// use App\Events\ArticleReadyToPublish; // Class no longer exists
|
// use App\Events\ArticleReadyToPublish; // Class no longer exists
|
||||||
use App\Events\ExceptionLogged;
|
use App\Events\ExceptionLogged;
|
||||||
|
|
@ -268,6 +269,9 @@ public function test_log_exception_to_database_listener_creates_log(): void
|
||||||
public function test_event_listener_registration_works(): void
|
public function test_event_listener_registration_works(): void
|
||||||
{
|
{
|
||||||
// Test that events are properly bound to listeners
|
// Test that events are properly bound to listeners
|
||||||
|
$listeners = Event::getListeners(ActionPerformed::class);
|
||||||
|
$this->assertNotEmpty($listeners);
|
||||||
|
|
||||||
$listeners = Event::getListeners(NewArticleFetched::class);
|
$listeners = Event::getListeners(NewArticleFetched::class);
|
||||||
$this->assertNotEmpty($listeners);
|
$this->assertNotEmpty($listeners);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ public function test_creates_belga_feed_with_correct_url(): void
|
||||||
|
|
||||||
$feed = $this->action->execute('Belga News', 'belga', $language->id);
|
$feed = $this->action->execute('Belga News', 'belga', $language->id);
|
||||||
|
|
||||||
$this->assertEquals('https://www.belganewsagency.eu/', $feed->url);
|
$this->assertEquals('https://www.belganewsagency.eu/feed', $feed->url);
|
||||||
$this->assertEquals('website', $feed->type);
|
$this->assertEquals('rss', $feed->type);
|
||||||
$this->assertEquals('belga', $feed->provider);
|
$this->assertEquals('belga', $feed->provider);
|
||||||
$this->assertNull($feed->description);
|
$this->assertNull($feed->description);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,4 +86,14 @@ public function test_enum_value_is_string_backed(): void
|
||||||
{
|
{
|
||||||
$this->assertIsString(PlatformEnum::LEMMY->value);
|
$this->assertIsString(PlatformEnum::LEMMY->value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_channel_label_returns_community_for_lemmy(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('Community', PlatformEnum::LEMMY->channelLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_channel_label_plural_returns_communities_for_lemmy(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('Communities', PlatformEnum::LEMMY->channelLabelPlural());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
tests/Unit/Events/ActionPerformedTest.php
Normal file
48
tests/Unit/Events/ActionPerformedTest.php
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Events;
|
||||||
|
|
||||||
|
use App\Enums\LogLevelEnum;
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ActionPerformedTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_event_can_be_constructed_with_defaults(): void
|
||||||
|
{
|
||||||
|
$event = new ActionPerformed('Test message');
|
||||||
|
|
||||||
|
$this->assertEquals('Test message', $event->message);
|
||||||
|
$this->assertEquals(LogLevelEnum::INFO, $event->level);
|
||||||
|
$this->assertEquals([], $event->context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_event_can_be_constructed_with_custom_level_and_context(): void
|
||||||
|
{
|
||||||
|
$context = ['article_id' => 1, 'error' => 'Something failed'];
|
||||||
|
|
||||||
|
$event = new ActionPerformed(
|
||||||
|
'Article validation failed',
|
||||||
|
LogLevelEnum::ERROR,
|
||||||
|
$context,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals('Article validation failed', $event->message);
|
||||||
|
$this->assertEquals(LogLevelEnum::ERROR, $event->level);
|
||||||
|
$this->assertEquals($context, $event->context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_event_uses_dispatchable_trait(): void
|
||||||
|
{
|
||||||
|
$this->assertContains(Dispatchable::class, class_uses(ActionPerformed::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_event_does_not_use_serializes_models_trait(): void
|
||||||
|
{
|
||||||
|
$this->assertNotContains(
|
||||||
|
\Illuminate\Queue\SerializesModels::class,
|
||||||
|
class_uses(ActionPerformed::class),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
tests/Unit/Listeners/LogActionListenerTest.php
Normal file
85
tests/Unit/Listeners/LogActionListenerTest.php
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Listeners;
|
||||||
|
|
||||||
|
use App\Enums\LogLevelEnum;
|
||||||
|
use App\Events\ActionPerformed;
|
||||||
|
use App\Listeners\LogActionListener;
|
||||||
|
use App\Models\Log;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class LogActionListenerTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_listener_creates_log_entry_with_correct_data(): void
|
||||||
|
{
|
||||||
|
$event = new ActionPerformed(
|
||||||
|
'Article published successfully',
|
||||||
|
LogLevelEnum::INFO,
|
||||||
|
['article_id' => 42],
|
||||||
|
);
|
||||||
|
|
||||||
|
$listener = app(LogActionListener::class);
|
||||||
|
$listener->handle($event);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('logs', [
|
||||||
|
'level' => 'info',
|
||||||
|
'message' => 'Article published successfully',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$log = Log::where('message', 'Article published successfully')->first();
|
||||||
|
$this->assertEquals(['article_id' => 42], $log->context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_listener_creates_log_with_warning_level(): void
|
||||||
|
{
|
||||||
|
$event = new ActionPerformed(
|
||||||
|
'No publications created',
|
||||||
|
LogLevelEnum::WARNING,
|
||||||
|
['article_id' => 7],
|
||||||
|
);
|
||||||
|
|
||||||
|
$listener = app(LogActionListener::class);
|
||||||
|
$listener->handle($event);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('logs', [
|
||||||
|
'level' => 'warning',
|
||||||
|
'message' => 'No publications created',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_listener_creates_log_with_error_level(): void
|
||||||
|
{
|
||||||
|
$event = new ActionPerformed(
|
||||||
|
'Publishing failed',
|
||||||
|
LogLevelEnum::ERROR,
|
||||||
|
['error' => 'Connection timeout'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$listener = app(LogActionListener::class);
|
||||||
|
$listener->handle($event);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('logs', [
|
||||||
|
'level' => 'error',
|
||||||
|
'message' => 'Publishing failed',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_listener_creates_log_with_empty_context(): void
|
||||||
|
{
|
||||||
|
$event = new ActionPerformed('Simple action');
|
||||||
|
|
||||||
|
$listener = app(LogActionListener::class);
|
||||||
|
$listener->handle($event);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('logs', [
|
||||||
|
'level' => 'info',
|
||||||
|
'message' => 'Simple action',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$log = Log::where('message', 'Simple action')->first();
|
||||||
|
$this->assertEquals([], $log->context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -156,6 +156,52 @@ public function test_get_articles_from_rss_feed_handles_http_failure(): void
|
||||||
$this->assertEmpty($result);
|
$this->assertEmpty($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_get_articles_from_belga_rss_feed_creates_articles(): void
|
||||||
|
{
|
||||||
|
$belgaRss = <<<'XML'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>Belga News Agency</title>
|
||||||
|
<link>https://www.belganewsagency.eu</link>
|
||||||
|
<item>
|
||||||
|
<title>Belgium announces new climate plan</title>
|
||||||
|
<link>https://www.belganewsagency.eu/belgium-announces-new-climate-plan</link>
|
||||||
|
<description>Belgium has unveiled a comprehensive climate strategy.</description>
|
||||||
|
<pubDate>Sun, 08 Mar 2026 10:00:00 GMT</pubDate>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>EU summit concludes in Brussels</title>
|
||||||
|
<link>https://www.belganewsagency.eu/eu-summit-concludes-in-brussels</link>
|
||||||
|
<description>European leaders reached agreement on key issues.</description>
|
||||||
|
<pubDate>Sun, 08 Mar 2026 09:00:00 GMT</pubDate>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
Http::fake(['*' => Http::response($belgaRss, 200)]);
|
||||||
|
|
||||||
|
$feed = Feed::factory()->create([
|
||||||
|
'type' => 'rss',
|
||||||
|
'provider' => 'belga',
|
||||||
|
'url' => 'https://www.belganewsagency.eu/feed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$fetcher = $this->createArticleFetcher();
|
||||||
|
$result = $fetcher->getArticlesFromFeed($feed);
|
||||||
|
|
||||||
|
$this->assertCount(2, $result);
|
||||||
|
$this->assertDatabaseHas('articles', [
|
||||||
|
'url' => 'https://www.belganewsagency.eu/belgium-announces-new-climate-plan',
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('articles', [
|
||||||
|
'url' => 'https://www.belganewsagency.eu/eu-summit-concludes-in-brussels',
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
Mockery::close();
|
Mockery::close();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue