2025-08-15 02:50:42 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Tests\Unit\Jobs;
|
|
|
|
|
|
2026-03-09 21:20:41 +01:00
|
|
|
use App\Enums\NotificationSeverityEnum;
|
|
|
|
|
use App\Enums\NotificationTypeEnum;
|
2025-08-15 02:50:42 +02:00
|
|
|
use App\Exceptions\PublishException;
|
|
|
|
|
use App\Jobs\PublishNextArticleJob;
|
|
|
|
|
use App\Models\Article;
|
|
|
|
|
use App\Models\ArticlePublication;
|
|
|
|
|
use App\Models\Feed;
|
2026-03-09 21:20:41 +01:00
|
|
|
use App\Models\Notification;
|
2026-03-18 16:05:31 +01:00
|
|
|
use App\Models\Route;
|
|
|
|
|
use App\Models\RouteArticle;
|
2026-03-08 11:25:50 +01:00
|
|
|
use App\Models\Setting;
|
2025-08-15 02:50:42 +02:00
|
|
|
use App\Services\Article\ArticleFetcher;
|
2026-03-09 21:20:41 +01:00
|
|
|
use App\Services\Notification\NotificationService;
|
2025-08-15 02:50:42 +02:00
|
|
|
use App\Services\Publishing\ArticlePublishingService;
|
2026-03-18 16:05:31 +01:00
|
|
|
use Illuminate\Foundation\Queue\Queueable;
|
2025-08-15 02:50:42 +02:00
|
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
|
use Mockery;
|
|
|
|
|
use Tests\TestCase;
|
|
|
|
|
|
|
|
|
|
class PublishNextArticleJobTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
use RefreshDatabase;
|
|
|
|
|
|
2026-03-09 21:20:41 +01:00
|
|
|
private NotificationService $notificationService;
|
|
|
|
|
|
2025-08-15 02:50:42 +02:00
|
|
|
protected function setUp(): void
|
|
|
|
|
{
|
|
|
|
|
parent::setUp();
|
2026-03-09 21:20:41 +01:00
|
|
|
$this->notificationService = new NotificationService;
|
2025-08-15 02:50:42 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:05:31 +01:00
|
|
|
private function createApprovedRouteArticle(array $articleOverrides = [], array $routeOverrides = []): RouteArticle
|
|
|
|
|
{
|
|
|
|
|
$feed = Feed::factory()->create();
|
|
|
|
|
$route = Route::factory()->active()->create(array_merge(['feed_id' => $feed->id], $routeOverrides));
|
|
|
|
|
$article = Article::factory()->create(array_merge(['feed_id' => $feed->id], $articleOverrides));
|
|
|
|
|
|
|
|
|
|
return RouteArticle::factory()->forRoute($route)->approved()->create([
|
|
|
|
|
'article_id' => $article->id,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-15 02:50:42 +02:00
|
|
|
public function test_constructor_sets_correct_queue(): void
|
|
|
|
|
{
|
2026-03-08 14:18:28 +01:00
|
|
|
$job = new PublishNextArticleJob;
|
|
|
|
|
|
2025-08-15 02:50:42 +02:00
|
|
|
$this->assertEquals('publishing', $job->queue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_job_implements_should_queue(): void
|
|
|
|
|
{
|
2026-03-08 14:18:28 +01:00
|
|
|
$job = new PublishNextArticleJob;
|
|
|
|
|
|
2025-08-15 02:50:42 +02:00
|
|
|
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_job_implements_should_be_unique(): void
|
|
|
|
|
{
|
2026-03-08 14:18:28 +01:00
|
|
|
$job = new PublishNextArticleJob;
|
|
|
|
|
|
2025-08-15 02:50:42 +02:00
|
|
|
$this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldBeUnique::class, $job);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_job_has_unique_for_property(): void
|
|
|
|
|
{
|
2026-03-08 14:18:28 +01:00
|
|
|
$job = new PublishNextArticleJob;
|
|
|
|
|
|
2025-08-15 02:50:42 +02:00
|
|
|
$this->assertEquals(300, $job->uniqueFor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_job_uses_queueable_trait(): void
|
|
|
|
|
{
|
2026-03-08 14:18:28 +01:00
|
|
|
$job = new PublishNextArticleJob;
|
|
|
|
|
|
2026-03-18 16:05:31 +01:00
|
|
|
$this->assertContains(Queueable::class, class_uses($job));
|
2025-08-15 02:50:42 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:05:31 +01:00
|
|
|
public function test_handle_returns_early_when_no_approved_route_articles(): void
|
2025-08-15 02:50:42 +02:00
|
|
|
{
|
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
2026-03-18 16:05:31 +01:00
|
|
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
2026-03-08 14:18:28 +01:00
|
|
|
|
|
|
|
|
$job = new PublishNextArticleJob;
|
2026-03-09 21:20:41 +01:00
|
|
|
$job->handle($articleFetcherMock, $publishingServiceMock, $this->notificationService);
|
2025-08-15 02:50:42 +02:00
|
|
|
|
|
|
|
|
$this->assertTrue(true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:05:31 +01:00
|
|
|
public function test_handle_returns_early_when_no_unpublished_approved_route_articles(): void
|
2025-08-15 02:50:42 +02:00
|
|
|
{
|
2026-03-18 16:05:31 +01:00
|
|
|
$routeArticle = $this->createApprovedRouteArticle();
|
2026-03-08 14:18:28 +01:00
|
|
|
|
2026-03-18 16:05:31 +01:00
|
|
|
// Mark the article as already published to this channel
|
|
|
|
|
ArticlePublication::factory()->create([
|
|
|
|
|
'article_id' => $routeArticle->article_id,
|
|
|
|
|
'platform_channel_id' => $routeArticle->platform_channel_id,
|
|
|
|
|
]);
|
2025-08-15 02:50:42 +02:00
|
|
|
|
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
2026-03-18 16:05:31 +01:00
|
|
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
2026-03-08 14:18:28 +01:00
|
|
|
|
|
|
|
|
$job = new PublishNextArticleJob;
|
2026-03-09 21:20:41 +01:00
|
|
|
$job->handle($articleFetcherMock, $publishingServiceMock, $this->notificationService);
|
2025-08-15 02:50:42 +02:00
|
|
|
|
|
|
|
|
$this->assertTrue(true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:05:31 +01:00
|
|
|
public function test_handle_skips_non_approved_route_articles(): void
|
2025-08-15 02:50:42 +02:00
|
|
|
{
|
|
|
|
|
$feed = Feed::factory()->create();
|
2026-03-18 16:05:31 +01:00
|
|
|
$route = Route::factory()->active()->create(['feed_id' => $feed->id]);
|
|
|
|
|
$article = Article::factory()->create(['feed_id' => $feed->id]);
|
|
|
|
|
|
|
|
|
|
RouteArticle::factory()->forRoute($route)->pending()->create(['article_id' => $article->id]);
|
2025-08-15 02:50:42 +02:00
|
|
|
|
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
2026-03-18 16:05:31 +01:00
|
|
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
2026-03-08 14:18:28 +01:00
|
|
|
|
|
|
|
|
$job = new PublishNextArticleJob;
|
2026-03-09 21:20:41 +01:00
|
|
|
$job->handle($articleFetcherMock, $publishingServiceMock, $this->notificationService);
|
2025-08-15 02:50:42 +02:00
|
|
|
|
|
|
|
|
$this->assertTrue(true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:05:31 +01:00
|
|
|
public function test_handle_publishes_oldest_approved_route_article(): void
|
2025-08-15 02:50:42 +02:00
|
|
|
{
|
|
|
|
|
$feed = Feed::factory()->create();
|
2026-03-18 16:05:31 +01:00
|
|
|
$route = Route::factory()->active()->create(['feed_id' => $feed->id]);
|
|
|
|
|
|
|
|
|
|
$olderArticle = Article::factory()->create(['feed_id' => $feed->id]);
|
|
|
|
|
$newerArticle = Article::factory()->create(['feed_id' => $feed->id]);
|
2026-03-08 14:18:28 +01:00
|
|
|
|
2026-03-18 16:05:31 +01:00
|
|
|
RouteArticle::factory()->forRoute($route)->approved()->create([
|
|
|
|
|
'article_id' => $olderArticle->id,
|
2026-03-08 14:18:28 +01:00
|
|
|
'created_at' => now()->subHours(2),
|
2025-08-15 02:50:42 +02:00
|
|
|
]);
|
2026-03-18 16:05:31 +01:00
|
|
|
RouteArticle::factory()->forRoute($route)->approved()->create([
|
|
|
|
|
'article_id' => $newerArticle->id,
|
2026-03-08 14:18:28 +01:00
|
|
|
'created_at' => now()->subHour(),
|
2025-08-15 02:50:42 +02:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$extractedData = ['title' => 'Test Article', 'content' => 'Test content'];
|
|
|
|
|
|
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
|
|
|
|
$articleFetcherMock->shouldReceive('fetchArticleData')
|
|
|
|
|
->once()
|
2026-03-18 16:05:31 +01:00
|
|
|
->with(Mockery::on(fn ($article) => $article->id === $olderArticle->id))
|
2025-08-15 02:50:42 +02:00
|
|
|
->andReturn($extractedData);
|
|
|
|
|
|
|
|
|
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
2026-03-18 16:05:31 +01:00
|
|
|
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
2025-08-15 02:50:42 +02:00
|
|
|
->once()
|
|
|
|
|
->with(
|
2026-03-18 16:05:31 +01:00
|
|
|
Mockery::on(fn ($ra) => $ra->article_id === $olderArticle->id),
|
2025-08-15 02:50:42 +02:00
|
|
|
$extractedData
|
2026-03-09 21:20:41 +01:00
|
|
|
)
|
2026-03-18 16:05:31 +01:00
|
|
|
->andReturn(ArticlePublication::factory()->make());
|
2025-08-15 02:50:42 +02:00
|
|
|
|
2026-03-08 14:18:28 +01:00
|
|
|
$job = new PublishNextArticleJob;
|
2026-03-09 21:20:41 +01:00
|
|
|
$job->handle($articleFetcherMock, $publishingServiceMock, $this->notificationService);
|
2025-08-15 02:50:42 +02:00
|
|
|
|
|
|
|
|
$this->assertTrue(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_handle_throws_exception_on_publishing_failure(): void
|
|
|
|
|
{
|
2026-03-18 16:05:31 +01:00
|
|
|
$routeArticle = $this->createApprovedRouteArticle();
|
|
|
|
|
$article = $routeArticle->article;
|
2025-08-15 02:50:42 +02:00
|
|
|
|
|
|
|
|
$extractedData = ['title' => 'Test Article'];
|
|
|
|
|
$publishException = new PublishException($article, null);
|
|
|
|
|
|
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
|
|
|
|
$articleFetcherMock->shouldReceive('fetchArticleData')
|
|
|
|
|
->once()
|
|
|
|
|
->andReturn($extractedData);
|
|
|
|
|
|
|
|
|
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
2026-03-18 16:05:31 +01:00
|
|
|
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
2025-08-15 02:50:42 +02:00
|
|
|
->once()
|
|
|
|
|
->andThrow($publishException);
|
|
|
|
|
|
2026-03-08 14:18:28 +01:00
|
|
|
$job = new PublishNextArticleJob;
|
2025-08-15 02:50:42 +02:00
|
|
|
|
|
|
|
|
$this->expectException(PublishException::class);
|
|
|
|
|
|
2026-03-09 21:20:41 +01:00
|
|
|
$job->handle($articleFetcherMock, $publishingServiceMock, $this->notificationService);
|
2025-08-15 02:50:42 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-08 11:25:50 +01:00
|
|
|
public function test_handle_skips_publishing_when_last_publication_within_interval(): void
|
|
|
|
|
{
|
2026-03-18 16:05:31 +01:00
|
|
|
$this->createApprovedRouteArticle();
|
2026-03-08 11:25:50 +01:00
|
|
|
|
|
|
|
|
ArticlePublication::factory()->create([
|
|
|
|
|
'published_at' => now()->subMinutes(3),
|
|
|
|
|
]);
|
|
|
|
|
Setting::setArticlePublishingInterval(10);
|
|
|
|
|
|
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
|
|
|
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
|
|
|
|
|
|
|
|
|
$articleFetcherMock->shouldNotReceive('fetchArticleData');
|
2026-03-18 16:05:31 +01:00
|
|
|
$publishingServiceMock->shouldNotReceive('publishRouteArticle');
|
2026-03-08 11:25:50 +01:00
|
|
|
|
2026-03-08 14:18:28 +01:00
|
|
|
$job = new PublishNextArticleJob;
|
2026-03-09 21:20:41 +01:00
|
|
|
$job->handle($articleFetcherMock, $publishingServiceMock, $this->notificationService);
|
2026-03-08 11:25:50 +01:00
|
|
|
|
|
|
|
|
$this->assertTrue(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_handle_publishes_when_last_publication_beyond_interval(): void
|
|
|
|
|
{
|
2026-03-18 16:05:31 +01:00
|
|
|
$this->createApprovedRouteArticle();
|
2026-03-08 11:25:50 +01:00
|
|
|
|
|
|
|
|
ArticlePublication::factory()->create([
|
|
|
|
|
'published_at' => now()->subMinutes(15),
|
|
|
|
|
]);
|
|
|
|
|
Setting::setArticlePublishingInterval(10);
|
|
|
|
|
|
|
|
|
|
$extractedData = ['title' => 'Test Article'];
|
|
|
|
|
|
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
|
|
|
|
$articleFetcherMock->shouldReceive('fetchArticleData')
|
|
|
|
|
->once()
|
|
|
|
|
->andReturn($extractedData);
|
|
|
|
|
|
|
|
|
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
2026-03-18 16:05:31 +01:00
|
|
|
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
2026-03-09 21:20:41 +01:00
|
|
|
->once()
|
2026-03-18 16:05:31 +01:00
|
|
|
->andReturn(ArticlePublication::factory()->make());
|
2026-03-08 11:25:50 +01:00
|
|
|
|
2026-03-08 14:18:28 +01:00
|
|
|
$job = new PublishNextArticleJob;
|
2026-03-09 21:20:41 +01:00
|
|
|
$job->handle($articleFetcherMock, $publishingServiceMock, $this->notificationService);
|
2026-03-08 11:25:50 +01:00
|
|
|
|
|
|
|
|
$this->assertTrue(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_handle_publishes_when_interval_is_zero(): void
|
|
|
|
|
{
|
2026-03-18 16:05:31 +01:00
|
|
|
$this->createApprovedRouteArticle();
|
2026-03-08 11:25:50 +01:00
|
|
|
|
|
|
|
|
ArticlePublication::factory()->create([
|
|
|
|
|
'published_at' => now(),
|
|
|
|
|
]);
|
|
|
|
|
Setting::setArticlePublishingInterval(0);
|
|
|
|
|
|
|
|
|
|
$extractedData = ['title' => 'Test Article'];
|
|
|
|
|
|
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
|
|
|
|
$articleFetcherMock->shouldReceive('fetchArticleData')
|
|
|
|
|
->once()
|
|
|
|
|
->andReturn($extractedData);
|
|
|
|
|
|
|
|
|
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
2026-03-18 16:05:31 +01:00
|
|
|
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
|
|
|
|
->once()
|
|
|
|
|
->andReturn(ArticlePublication::factory()->make());
|
2026-03-08 11:25:50 +01:00
|
|
|
|
2026-03-08 14:18:28 +01:00
|
|
|
$job = new PublishNextArticleJob;
|
2026-03-09 21:20:41 +01:00
|
|
|
$job->handle($articleFetcherMock, $publishingServiceMock, $this->notificationService);
|
2026-03-08 11:25:50 +01:00
|
|
|
|
|
|
|
|
$this->assertTrue(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_handle_publishes_when_last_publication_exactly_at_interval(): void
|
|
|
|
|
{
|
2026-03-18 16:05:31 +01:00
|
|
|
$this->createApprovedRouteArticle();
|
2026-03-08 11:25:50 +01:00
|
|
|
|
|
|
|
|
ArticlePublication::factory()->create([
|
|
|
|
|
'published_at' => now()->subMinutes(10),
|
|
|
|
|
]);
|
|
|
|
|
Setting::setArticlePublishingInterval(10);
|
|
|
|
|
|
|
|
|
|
$extractedData = ['title' => 'Test Article'];
|
|
|
|
|
|
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
|
|
|
|
$articleFetcherMock->shouldReceive('fetchArticleData')
|
|
|
|
|
->once()
|
|
|
|
|
->andReturn($extractedData);
|
|
|
|
|
|
|
|
|
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
2026-03-18 16:05:31 +01:00
|
|
|
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
2026-03-09 21:20:41 +01:00
|
|
|
->once()
|
2026-03-18 16:05:31 +01:00
|
|
|
->andReturn(ArticlePublication::factory()->make());
|
2026-03-08 11:25:50 +01:00
|
|
|
|
2026-03-08 14:18:28 +01:00
|
|
|
$job = new PublishNextArticleJob;
|
2026-03-09 21:20:41 +01:00
|
|
|
$job->handle($articleFetcherMock, $publishingServiceMock, $this->notificationService);
|
2026-03-08 11:25:50 +01:00
|
|
|
|
|
|
|
|
$this->assertTrue(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_handle_publishes_when_no_previous_publications_exist(): void
|
|
|
|
|
{
|
2026-03-18 16:05:31 +01:00
|
|
|
$this->createApprovedRouteArticle();
|
2026-03-08 11:25:50 +01:00
|
|
|
|
|
|
|
|
Setting::setArticlePublishingInterval(10);
|
|
|
|
|
|
|
|
|
|
$extractedData = ['title' => 'Test Article'];
|
|
|
|
|
|
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
|
|
|
|
$articleFetcherMock->shouldReceive('fetchArticleData')
|
|
|
|
|
->once()
|
|
|
|
|
->andReturn($extractedData);
|
|
|
|
|
|
|
|
|
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
2026-03-18 16:05:31 +01:00
|
|
|
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
2026-03-09 21:20:41 +01:00
|
|
|
->once()
|
2026-03-18 16:05:31 +01:00
|
|
|
->andReturn(ArticlePublication::factory()->make());
|
2026-03-08 11:25:50 +01:00
|
|
|
|
2026-03-08 14:18:28 +01:00
|
|
|
$job = new PublishNextArticleJob;
|
2026-03-09 21:20:41 +01:00
|
|
|
$job->handle($articleFetcherMock, $publishingServiceMock, $this->notificationService);
|
2026-03-08 11:25:50 +01:00
|
|
|
|
|
|
|
|
$this->assertTrue(true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:05:31 +01:00
|
|
|
public function test_handle_creates_warning_notification_when_no_publication_created(): void
|
2026-03-09 21:20:41 +01:00
|
|
|
{
|
2026-03-18 16:05:31 +01:00
|
|
|
$routeArticle = $this->createApprovedRouteArticle(['title' => 'No Route Article']);
|
2026-03-09 21:20:41 +01:00
|
|
|
|
|
|
|
|
$extractedData = ['title' => 'No Route Article'];
|
|
|
|
|
|
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
|
|
|
|
$articleFetcherMock->shouldReceive('fetchArticleData')
|
|
|
|
|
->once()
|
|
|
|
|
->andReturn($extractedData);
|
|
|
|
|
|
|
|
|
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
2026-03-18 16:05:31 +01:00
|
|
|
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
2026-03-09 21:20:41 +01:00
|
|
|
->once()
|
2026-03-18 16:05:31 +01:00
|
|
|
->andReturn(null);
|
2026-03-09 21:20:41 +01:00
|
|
|
|
|
|
|
|
$job = new PublishNextArticleJob;
|
|
|
|
|
$job->handle($articleFetcherMock, $publishingServiceMock, $this->notificationService);
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('notifications', [
|
|
|
|
|
'type' => NotificationTypeEnum::PUBLISH_FAILED->value,
|
|
|
|
|
'severity' => NotificationSeverityEnum::WARNING->value,
|
2026-03-18 16:05:31 +01:00
|
|
|
'notifiable_type' => $routeArticle->article->getMorphClass(),
|
|
|
|
|
'notifiable_id' => $routeArticle->article_id,
|
2026-03-09 21:20:41 +01:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$notification = Notification::first();
|
|
|
|
|
$this->assertStringContainsString('No Route Article', $notification->title);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_handle_creates_notification_on_publish_exception(): void
|
|
|
|
|
{
|
2026-03-18 16:05:31 +01:00
|
|
|
$routeArticle = $this->createApprovedRouteArticle(['title' => 'Failing Article']);
|
|
|
|
|
$article = $routeArticle->article;
|
2026-03-09 21:20:41 +01:00
|
|
|
|
|
|
|
|
$extractedData = ['title' => 'Failing Article'];
|
|
|
|
|
$publishException = new PublishException($article, null);
|
|
|
|
|
|
|
|
|
|
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
|
|
|
|
$articleFetcherMock->shouldReceive('fetchArticleData')
|
|
|
|
|
->once()
|
|
|
|
|
->andReturn($extractedData);
|
|
|
|
|
|
|
|
|
|
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
2026-03-18 16:05:31 +01:00
|
|
|
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
2026-03-09 21:20:41 +01:00
|
|
|
->once()
|
|
|
|
|
->andThrow($publishException);
|
|
|
|
|
|
|
|
|
|
$job = new PublishNextArticleJob;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$job->handle($articleFetcherMock, $publishingServiceMock, $this->notificationService);
|
|
|
|
|
} catch (PublishException) {
|
|
|
|
|
// Expected
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('notifications', [
|
|
|
|
|
'type' => NotificationTypeEnum::PUBLISH_FAILED->value,
|
|
|
|
|
'severity' => NotificationSeverityEnum::ERROR->value,
|
|
|
|
|
'notifiable_type' => $article->getMorphClass(),
|
|
|
|
|
'notifiable_id' => $article->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$notification = Notification::first();
|
|
|
|
|
$this->assertStringContainsString('Failing Article', $notification->title);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 16:05:31 +01:00
|
|
|
public function test_job_can_be_serialized(): void
|
|
|
|
|
{
|
|
|
|
|
$job = new PublishNextArticleJob;
|
|
|
|
|
|
|
|
|
|
$serialized = serialize($job);
|
|
|
|
|
$unserialized = unserialize($serialized);
|
|
|
|
|
|
|
|
|
|
$this->assertInstanceOf(PublishNextArticleJob::class, $unserialized);
|
|
|
|
|
$this->assertEquals($job->queue, $unserialized->queue);
|
|
|
|
|
$this->assertEquals($job->uniqueFor, $unserialized->uniqueFor);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-15 02:50:42 +02:00
|
|
|
protected function tearDown(): void
|
|
|
|
|
{
|
|
|
|
|
Mockery::close();
|
|
|
|
|
parent::tearDown();
|
|
|
|
|
}
|
2026-03-08 14:18:28 +01:00
|
|
|
}
|