301 lines
9.6 KiB
PHP
301 lines
9.6 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace Tests\Feature;
|
||
|
|
|
||
|
|
use App\Events\ArticleApproved;
|
||
|
|
use App\Events\ArticleReadyToPublish;
|
||
|
|
use App\Events\ExceptionLogged;
|
||
|
|
use App\Events\ExceptionOccurred;
|
||
|
|
use App\Events\NewArticleFetched;
|
||
|
|
use App\Jobs\ArticleDiscoveryJob;
|
||
|
|
use App\Jobs\ArticleDiscoveryForFeedJob;
|
||
|
|
use App\Jobs\PublishToLemmyJob;
|
||
|
|
use App\Jobs\SyncChannelPostsJob;
|
||
|
|
use App\Listeners\LogExceptionToDatabase;
|
||
|
|
use App\Listeners\PublishApprovedArticle;
|
||
|
|
use App\Listeners\PublishArticle;
|
||
|
|
use App\Listeners\ValidateArticleListener;
|
||
|
|
use App\Models\Article;
|
||
|
|
use App\Models\Feed;
|
||
|
|
use App\Models\PlatformChannel;
|
||
|
|
use App\Models\Log;
|
||
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
|
|
use Illuminate\Support\Facades\Event;
|
||
|
|
use Illuminate\Support\Facades\Queue;
|
||
|
|
use Tests\TestCase;
|
||
|
|
|
||
|
|
class JobsAndEventsTest extends TestCase
|
||
|
|
{
|
||
|
|
use RefreshDatabase;
|
||
|
|
|
||
|
|
public function test_article_discovery_job_processes_successfully(): void
|
||
|
|
{
|
||
|
|
Queue::fake();
|
||
|
|
|
||
|
|
$feed = Feed::factory()->create(['is_active' => true]);
|
||
|
|
|
||
|
|
$job = new ArticleDiscoveryJob();
|
||
|
|
$job->handle();
|
||
|
|
|
||
|
|
// Should dispatch individual feed jobs
|
||
|
|
Queue::assertPushed(ArticleDiscoveryForFeedJob::class);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_article_discovery_for_feed_job_processes_feed(): void
|
||
|
|
{
|
||
|
|
Event::fake();
|
||
|
|
|
||
|
|
$feed = Feed::factory()->create([
|
||
|
|
'url' => 'https://example.com/feed',
|
||
|
|
'is_active' => true
|
||
|
|
]);
|
||
|
|
|
||
|
|
$job = new ArticleDiscoveryForFeedJob($feed);
|
||
|
|
|
||
|
|
// Mock the external dependency behavior
|
||
|
|
$this->app->bind(\App\Services\Article\ArticleFetcher::class, function () {
|
||
|
|
$mock = \Mockery::mock(\App\Services\Article\ArticleFetcher::class);
|
||
|
|
$mock->shouldReceive('fetchArticles')->andReturn([
|
||
|
|
['title' => 'Test Article', 'url' => 'https://example.com/article1'],
|
||
|
|
['title' => 'Another Article', 'url' => 'https://example.com/article2']
|
||
|
|
]);
|
||
|
|
return $mock;
|
||
|
|
});
|
||
|
|
|
||
|
|
$job->handle();
|
||
|
|
|
||
|
|
// Should create articles and fire events
|
||
|
|
$this->assertCount(2, Article::all());
|
||
|
|
Event::assertDispatched(NewArticleFetched::class, 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_sync_channel_posts_job_processes_successfully(): void
|
||
|
|
{
|
||
|
|
$channel = PlatformChannel::factory()->create();
|
||
|
|
|
||
|
|
$job = new SyncChannelPostsJob($channel);
|
||
|
|
|
||
|
|
// Mock the external dependency
|
||
|
|
$this->app->bind(\App\Modules\Lemmy\Services\LemmyApiService::class, function () {
|
||
|
|
$mock = \Mockery::mock(\App\Modules\Lemmy\Services\LemmyApiService::class);
|
||
|
|
$mock->shouldReceive('getChannelPosts')->andReturn([]);
|
||
|
|
return $mock;
|
||
|
|
});
|
||
|
|
|
||
|
|
$result = $job->handle();
|
||
|
|
|
||
|
|
$this->assertTrue($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_publish_to_lemmy_job_has_correct_configuration(): void
|
||
|
|
{
|
||
|
|
$article = Article::factory()->create();
|
||
|
|
|
||
|
|
$job = new PublishToLemmyJob($article);
|
||
|
|
|
||
|
|
$this->assertEquals('lemmy-posts', $job->queue);
|
||
|
|
$this->assertInstanceOf(PublishToLemmyJob::class, $job);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_new_article_fetched_event_is_dispatched(): void
|
||
|
|
{
|
||
|
|
Event::fake();
|
||
|
|
|
||
|
|
$feed = Feed::factory()->create();
|
||
|
|
$article = Article::factory()->create(['feed_id' => $feed->id]);
|
||
|
|
|
||
|
|
event(new NewArticleFetched($article));
|
||
|
|
|
||
|
|
Event::assertDispatched(NewArticleFetched::class, function (NewArticleFetched $event) use ($article) {
|
||
|
|
return $event->article->id === $article->id;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_article_approved_event_is_dispatched(): void
|
||
|
|
{
|
||
|
|
Event::fake();
|
||
|
|
|
||
|
|
$article = Article::factory()->create();
|
||
|
|
|
||
|
|
event(new ArticleApproved($article));
|
||
|
|
|
||
|
|
Event::assertDispatched(ArticleApproved::class, function (ArticleApproved $event) use ($article) {
|
||
|
|
return $event->article->id === $article->id;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_article_ready_to_publish_event_is_dispatched(): void
|
||
|
|
{
|
||
|
|
Event::fake();
|
||
|
|
|
||
|
|
$article = Article::factory()->create();
|
||
|
|
|
||
|
|
event(new ArticleReadyToPublish($article));
|
||
|
|
|
||
|
|
Event::assertDispatched(ArticleReadyToPublish::class, function (ArticleReadyToPublish $event) use ($article) {
|
||
|
|
return $event->article->id === $article->id;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_exception_occurred_event_is_dispatched(): void
|
||
|
|
{
|
||
|
|
Event::fake();
|
||
|
|
|
||
|
|
$exception = new \Exception('Test exception');
|
||
|
|
|
||
|
|
event(new ExceptionOccurred($exception, ['context' => 'test']));
|
||
|
|
|
||
|
|
Event::assertDispatched(ExceptionOccurred::class, function (ExceptionOccurred $event) {
|
||
|
|
return $event->exception->getMessage() === 'Test exception';
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_exception_logged_event_is_dispatched(): void
|
||
|
|
{
|
||
|
|
Event::fake();
|
||
|
|
|
||
|
|
$logData = [
|
||
|
|
'level' => 'error',
|
||
|
|
'message' => 'Test error',
|
||
|
|
'context' => ['key' => 'value']
|
||
|
|
];
|
||
|
|
|
||
|
|
event(new ExceptionLogged($logData));
|
||
|
|
|
||
|
|
Event::assertDispatched(ExceptionLogged::class, function (ExceptionLogged $event) use ($logData) {
|
||
|
|
return $event->logData['message'] === 'Test error';
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_validate_article_listener_processes_new_article(): void
|
||
|
|
{
|
||
|
|
Event::fake([ArticleReadyToPublish::class]);
|
||
|
|
|
||
|
|
$feed = Feed::factory()->create();
|
||
|
|
$article = Article::factory()->create([
|
||
|
|
'feed_id' => $feed->id,
|
||
|
|
'is_valid' => null,
|
||
|
|
'validated_at' => null
|
||
|
|
]);
|
||
|
|
|
||
|
|
$listener = new ValidateArticleListener();
|
||
|
|
$event = new NewArticleFetched($article);
|
||
|
|
|
||
|
|
$listener->handle($event);
|
||
|
|
|
||
|
|
$article->refresh();
|
||
|
|
$this->assertNotNull($article->validated_at);
|
||
|
|
$this->assertNotNull($article->is_valid);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_publish_approved_article_listener_queues_job(): void
|
||
|
|
{
|
||
|
|
Queue::fake();
|
||
|
|
|
||
|
|
$article = Article::factory()->create([
|
||
|
|
'approval_status' => 'approved',
|
||
|
|
'is_valid' => true,
|
||
|
|
'validated_at' => now()
|
||
|
|
]);
|
||
|
|
|
||
|
|
$listener = new PublishApprovedArticle();
|
||
|
|
$event = new ArticleApproved($article);
|
||
|
|
|
||
|
|
$listener->handle($event);
|
||
|
|
|
||
|
|
Event::assertDispatched(ArticleReadyToPublish::class);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_publish_article_listener_queues_publish_job(): void
|
||
|
|
{
|
||
|
|
Queue::fake();
|
||
|
|
|
||
|
|
$article = Article::factory()->create([
|
||
|
|
'is_valid' => true,
|
||
|
|
'validated_at' => now()
|
||
|
|
]);
|
||
|
|
|
||
|
|
$listener = new PublishArticle();
|
||
|
|
$event = new ArticleReadyToPublish($article);
|
||
|
|
|
||
|
|
$listener->handle($event);
|
||
|
|
|
||
|
|
Queue::assertPushed(PublishToLemmyJob::class, function (PublishToLemmyJob $job) use ($article) {
|
||
|
|
return $job->article->id === $article->id;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_log_exception_to_database_listener_creates_log(): void
|
||
|
|
{
|
||
|
|
$logData = [
|
||
|
|
'level' => 'error',
|
||
|
|
'message' => 'Test exception message',
|
||
|
|
'context' => json_encode(['error' => 'details'])
|
||
|
|
];
|
||
|
|
|
||
|
|
$listener = new LogExceptionToDatabase();
|
||
|
|
$event = new ExceptionLogged($logData);
|
||
|
|
|
||
|
|
$listener->handle($event);
|
||
|
|
|
||
|
|
$this->assertDatabaseHas('logs', [
|
||
|
|
'level' => 'error',
|
||
|
|
'message' => 'Test exception message'
|
||
|
|
]);
|
||
|
|
|
||
|
|
$log = Log::where('message', 'Test exception message')->first();
|
||
|
|
$this->assertNotNull($log);
|
||
|
|
$this->assertEquals('error', $log->level);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_event_listener_registration_works(): void
|
||
|
|
{
|
||
|
|
// Test that events are properly bound to listeners
|
||
|
|
$listeners = Event::getListeners(NewArticleFetched::class);
|
||
|
|
$this->assertNotEmpty($listeners);
|
||
|
|
|
||
|
|
$listeners = Event::getListeners(ArticleApproved::class);
|
||
|
|
$this->assertNotEmpty($listeners);
|
||
|
|
|
||
|
|
$listeners = Event::getListeners(ArticleReadyToPublish::class);
|
||
|
|
$this->assertNotEmpty($listeners);
|
||
|
|
|
||
|
|
$listeners = Event::getListeners(ExceptionLogged::class);
|
||
|
|
$this->assertNotEmpty($listeners);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_job_retry_configuration(): void
|
||
|
|
{
|
||
|
|
$article = Article::factory()->create();
|
||
|
|
|
||
|
|
$job = new PublishToLemmyJob($article);
|
||
|
|
|
||
|
|
// Test that job has retry configuration
|
||
|
|
$this->assertObjectHasProperty('tries', $job);
|
||
|
|
$this->assertObjectHasProperty('backoff', $job);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_job_queue_configuration(): void
|
||
|
|
{
|
||
|
|
$feed = Feed::factory()->create();
|
||
|
|
$channel = PlatformChannel::factory()->create();
|
||
|
|
$article = Article::factory()->create();
|
||
|
|
|
||
|
|
$discoveryJob = new ArticleDiscoveryJob();
|
||
|
|
$feedJob = new ArticleDiscoveryForFeedJob($feed);
|
||
|
|
$publishJob = new PublishToLemmyJob($article);
|
||
|
|
$syncJob = new SyncChannelPostsJob($channel);
|
||
|
|
|
||
|
|
// Test queue assignments
|
||
|
|
$this->assertEquals('default', $discoveryJob->queue ?? 'default');
|
||
|
|
$this->assertEquals('discovery', $feedJob->queue ?? 'discovery');
|
||
|
|
$this->assertEquals('lemmy-posts', $publishJob->queue);
|
||
|
|
$this->assertEquals('sync', $syncJob->queue ?? 'sync');
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function tearDown(): void
|
||
|
|
{
|
||
|
|
\Mockery::close();
|
||
|
|
parent::tearDown();
|
||
|
|
}
|
||
|
|
}
|