make(); $job = new ArticleDiscoveryForFeedJob($feed); $this->assertEquals('feed-discovery', $job->queue); } public function test_job_implements_should_queue(): void { $feed = Feed::factory()->make(); $job = new ArticleDiscoveryForFeedJob($feed); $this->assertInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class, $job); } public function test_job_uses_queueable_trait(): void { $feed = Feed::factory()->make(); $job = new ArticleDiscoveryForFeedJob($feed); $this->assertContains( \Illuminate\Foundation\Queue\Queueable::class, class_uses($job) ); } public function test_handle_fetches_articles_and_updates_feed(): void { // Arrange $feed = Feed::factory()->create([ 'name' => 'Test Feed', 'url' => 'https://example.com/feed', 'last_fetched_at' => null ]); $mockArticles = collect(['article1', 'article2']); // Mock ArticleFetcher $articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock->shouldReceive('getArticlesFromFeed') ->once() ->with($feed) ->andReturn($mockArticles); // Mock LogSaver $logSaverMock = Mockery::mock(LogSaver::class); $logSaverMock->shouldReceive('info') ->with('Starting feed article fetch', null, [ 'feed_id' => $feed->id, 'feed_name' => $feed->name, 'feed_url' => $feed->url ]) ->once(); $logSaverMock->shouldReceive('info') ->with('Feed article fetch completed', null, [ 'feed_id' => $feed->id, 'feed_name' => $feed->name, 'articles_count' => 2 ]) ->once(); $job = new ArticleDiscoveryForFeedJob($feed); // Act $job->handle($logSaverMock, $articleFetcherMock); // Assert $feed->refresh(); $this->assertNotNull($feed->last_fetched_at); $this->assertTrue($feed->last_fetched_at->greaterThan(now()->subMinute())); } public function test_dispatch_for_all_active_feeds_dispatches_jobs_with_delay(): void { // Arrange $feeds = Feed::factory()->count(3)->create(['is_active' => true]); Feed::factory()->create(['is_active' => false]); // inactive feed should be ignored // Mock LogSaver $logSaverMock = Mockery::mock(LogSaver::class); $logSaverMock->shouldReceive('info') ->times(3) // Once for each active feed ->with('Dispatched feed discovery job', null, Mockery::type('array')); $this->app->instance(LogSaver::class, $logSaverMock); // Act ArticleDiscoveryForFeedJob::dispatchForAllActiveFeeds(); // Assert Queue::assertPushed(ArticleDiscoveryForFeedJob::class, 3); // Verify jobs were dispatched (cannot access private $feed property in test) } public function test_dispatch_for_all_active_feeds_applies_correct_delays(): void { // Arrange Feed::factory()->count(2)->create(['is_active' => true]); // Mock LogSaver $logSaverMock = Mockery::mock(LogSaver::class); $logSaverMock->shouldReceive('info')->times(2); $this->app->instance(LogSaver::class, $logSaverMock); // Act ArticleDiscoveryForFeedJob::dispatchForAllActiveFeeds(); // Assert Queue::assertPushed(ArticleDiscoveryForFeedJob::class, 2); // Verify jobs are pushed with delays Queue::assertPushed(ArticleDiscoveryForFeedJob::class, function ($job) { return $job->delay !== null; }); } public function test_dispatch_for_all_active_feeds_with_no_active_feeds(): void { // Arrange Feed::factory()->count(2)->create(['is_active' => false]); // Act ArticleDiscoveryForFeedJob::dispatchForAllActiveFeeds(); // Assert Queue::assertNothingPushed(); } public function test_feed_discovery_delay_constant_exists(): void { $reflection = new \ReflectionClass(ArticleDiscoveryForFeedJob::class); $constant = $reflection->getConstant('FEED_DISCOVERY_DELAY_MINUTES'); $this->assertEquals(5, $constant); } public function test_job_can_be_serialized(): void { $feed = Feed::factory()->create(['name' => 'Test Feed']); $job = new ArticleDiscoveryForFeedJob($feed); $serialized = serialize($job); $unserialized = unserialize($serialized); $this->assertInstanceOf(ArticleDiscoveryForFeedJob::class, $unserialized); $this->assertEquals($job->queue, $unserialized->queue); // Note: Cannot test feed property directly as it's private // but serialization/unserialization working proves the job structure is intact } public function test_handle_logs_start_message_with_correct_context(): void { // Arrange $feed = Feed::factory()->create([ 'name' => 'Test Feed', 'url' => 'https://example.com/feed' ]); $mockArticles = collect([]); // Mock ArticleFetcher $articleFetcherMock = Mockery::mock(ArticleFetcher::class); $articleFetcherMock->shouldReceive('getArticlesFromFeed') ->once() ->andReturn($mockArticles); // Mock LogSaver with specific expectations $logSaverMock = Mockery::mock(LogSaver::class); $logSaverMock->shouldReceive('info') ->with('Starting feed article fetch', null, [ 'feed_id' => $feed->id, 'feed_name' => 'Test Feed', 'feed_url' => 'https://example.com/feed' ]) ->once(); $logSaverMock->shouldReceive('info') ->with('Feed article fetch completed', null, Mockery::type('array')) ->once(); $job = new ArticleDiscoveryForFeedJob($feed); // Act $job->handle($logSaverMock, $articleFetcherMock); // Assert - Mockery expectations are verified in tearDown $this->assertTrue(true); } protected function tearDown(): void { Mockery::close(); parent::tearDown(); } }