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 ]); // Mock the ArticleFetcher service in the container $mockFetcher = \Mockery::mock(\App\Services\Article\ArticleFetcher::class); $article1 = Article::factory()->create(['url' => 'https://example.com/article1', 'feed_id' => $feed->id]); $article2 = Article::factory()->create(['url' => 'https://example.com/article2', 'feed_id' => $feed->id]); $mockFetcher->shouldReceive('getArticlesFromFeed') ->with($feed) ->andReturn(collect([$article1, $article2])); $this->app->instance(\App\Services\Article\ArticleFetcher::class, $mockFetcher); $job = new ArticleDiscoveryForFeedJob($feed); $job->handle(); // Should have articles in database (existing articles created by factory) $this->assertCount(2, Article::all()); // Note: Events are not fired by ArticleDiscoveryForFeedJob directly // They would be fired by the Article model when created } public function test_sync_channel_posts_job_processes_successfully(): void { // Verify encryption is properly configured $key = config('app.key'); $cipher = config('app.cipher'); $this->assertNotNull($key, 'APP_KEY should be set'); // The supported method expects the raw key, not the base64: prefixed version $rawKey = base64_decode(substr($key, 7)); // Remove 'base64:' prefix and decode $this->assertTrue(app('encrypter')->supported($rawKey, $cipher), 'Encryption should be supported'); $channel = PlatformChannel::factory()->create([ 'channel_id' => '' // Empty string to trigger getCommunityId call ]); // Create platform account with proper factory $account = \App\Models\PlatformAccount::factory()->create([ 'is_active' => true, 'username' => 'testuser', 'platform' => 'lemmy', 'instance_url' => 'https://lemmy.example.com' ]); // Attach the account to the channel with active status $channel->platformAccounts()->attach($account->id, [ 'is_active' => true, 'priority' => 1 ]); $job = new SyncChannelPostsJob($channel); // Mock the LemmyApiService class $mockApi = \Mockery::mock('overload:' . \App\Modules\Lemmy\Services\LemmyApiService::class); $mockApi->shouldReceive('login') ->with('testuser', 'test-password') // From factory default ->once() ->andReturn('fake-jwt-token'); $mockApi->shouldReceive('getCommunityId') ->once() ->andReturn(123); $mockApi->shouldReceive('syncChannelPosts') ->once() ->andReturn(true); $job->handle(); $this->assertTrue(true); // If we get here without exception, test passes } 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, \App\Enums\LogLevelEnum::ERROR, 'Test 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(); $log = Log::factory()->create([ 'level' => 'error', 'message' => 'Test error', 'context' => json_encode(['key' => 'value']) ]); event(new ExceptionLogged($log)); Event::assertDispatched(ExceptionLogged::class, function (ExceptionLogged $event) use ($log) { return $event->log->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 ]); // Mock ArticleFetcher to return valid article data $mockFetcher = \Mockery::mock('alias:ArticleFetcher2'); $this->app->instance(\App\Services\Article\ArticleFetcher::class, $mockFetcher); $mockFetcher->shouldReceive('fetchArticleData') ->with($article) ->andReturn([ 'full_article' => 'Test article content' ]); $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 { Event::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); } public function test_log_exception_to_database_listener_creates_log(): void { $log = Log::factory()->create([ 'level' => 'error', 'message' => 'Test exception message', 'context' => json_encode(['error' => 'details']) ]); $listener = new LogExceptionToDatabase(); $exception = new \Exception('Test exception message'); $event = new ExceptionOccurred($exception, \App\Enums\LogLevelEnum::ERROR, 'Test exception message'); $listener->handle($event); $this->assertDatabaseHas('logs', [ 'level' => 'error', 'message' => 'Test exception message' ]); $savedLog = Log::where('message', 'Test exception message')->first(); $this->assertNotNull($savedLog); $this->assertEquals(\App\Enums\LogLevelEnum::ERROR, $savedLog->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(ExceptionOccurred::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('feed-discovery', $discoveryJob->queue ?? 'default'); $this->assertEquals('feed-discovery', $feedJob->queue ?? 'discovery'); $this->assertEquals('lemmy-posts', $publishJob->queue); $this->assertEquals('sync', $syncJob->queue ?? 'sync'); } protected function tearDown(): void { \Mockery::close(); parent::tearDown(); } }