fedi-feed-router/tests/Unit/Jobs/SyncChannelPostsJobTest.php

276 lines
9.1 KiB
PHP

<?php
namespace Tests\Unit\Jobs;
use App\Enums\PlatformEnum;
use App\Jobs\SyncChannelPostsJob;
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use App\Models\PlatformInstance;
use App\Modules\Lemmy\Services\LemmyApiService;
use App\Services\Log\LogSaver;
use Exception;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Queue;
use Mockery;
use Tests\TestCase;
class SyncChannelPostsJobTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Queue::fake();
Cache::flush();
}
public function test_constructor_sets_correct_queue(): void
{
$channel = PlatformChannel::factory()->make();
$job = new SyncChannelPostsJob($channel);
$this->assertEquals('sync', $job->queue);
}
public function test_job_implements_should_queue(): void
{
$channel = PlatformChannel::factory()->make();
$job = new SyncChannelPostsJob($channel);
$this->assertInstanceOf(ShouldQueue::class, $job);
}
public function test_job_implements_should_be_unique(): void
{
$channel = PlatformChannel::factory()->make();
$job = new SyncChannelPostsJob($channel);
$this->assertInstanceOf(ShouldBeUnique::class, $job);
}
public function test_job_uses_queueable_trait(): void
{
$channel = PlatformChannel::factory()->make();
$job = new SyncChannelPostsJob($channel);
$this->assertContains(
Queueable::class,
class_uses($job)
);
}
public function test_dispatch_for_all_active_channels_dispatches_jobs(): void
{
// Arrange
$platformInstance = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY,
]);
$account = PlatformAccount::factory()->create([
'instance_url' => $platformInstance->url,
'is_active' => true,
]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id,
'is_active' => true,
]);
// Attach account to channel with active status
$channel->platformAccounts()->attach($account->id, [
'is_active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
// Mock LogSaver to avoid strict expectations
$logSaverMock = Mockery::mock(LogSaver::class);
$logSaverMock->shouldReceive('info')->zeroOrMoreTimes();
$this->app->instance(LogSaver::class, $logSaverMock);
// Act
SyncChannelPostsJob::dispatchForAllActiveChannels();
// Assert - At least one job should be dispatched
Queue::assertPushed(SyncChannelPostsJob::class);
}
public function test_handle_logs_start_message(): void
{
// Arrange
$platformInstance = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY,
'url' => 'https://lemmy.example.com',
]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id,
'name' => 'testcommunity',
]);
// Mock LogSaver - only test that logging methods are called
$logSaverMock = Mockery::mock(LogSaver::class);
$logSaverMock->shouldReceive('info')->atLeast()->once();
$logSaverMock->shouldReceive('error')->zeroOrMoreTimes();
$job = new SyncChannelPostsJob($channel);
// Act - This will fail due to no active account, but we test the logging
try {
$job->handle($logSaverMock);
} catch (Exception $e) {
// Expected to fail, we're testing that logging is called
}
// Assert - Test completes if no exceptions during setup
$this->assertTrue(true);
}
public function test_sync_resolves_non_numeric_channel_id_via_get_community_id(): void
{
[$channel, $account] = $this->makeSyncableChannel('tech_news');
$apiMock = Mockery::mock(LemmyApiService::class);
$apiMock->shouldReceive('login')
->once()
->with($account->username, $account->password)
->andReturn('token');
$apiMock->shouldReceive('getCommunityId')
->once()
->with('tech_news', 'token')
->andReturn(42);
$apiMock->shouldReceive('syncChannelPosts')
->once()
->with('token', 42, $channel->name);
$logSaverMock = Mockery::mock(LogSaver::class);
$logSaverMock->shouldReceive('info')->zeroOrMoreTimes();
$logSaverMock->shouldReceive('error')->zeroOrMoreTimes();
$this->makeJobWithApi($channel, $apiMock)->handle($logSaverMock);
// The behaviour under test is the mocked call sequence above; assert explicitly
// so PHPUnit does not flag the test as risky for performing no assertions.
$this->addToAssertionCount(1);
}
public function test_sync_uses_numeric_channel_id_directly_without_lookup(): void
{
[$channel, $account] = $this->makeSyncableChannel('42');
$apiMock = Mockery::mock(LemmyApiService::class);
$apiMock->shouldReceive('login')
->once()
->with($account->username, $account->password)
->andReturn('token');
// The behaviour that matters here is that no community lookup happens — the
// numeric channel_id is used as-is. (The int-ness of the argument is not worth
// asserting: syncChannelPosts() declares `int $platformChannelId`, so PHP coerces
// '42' at the call boundary whether or not the job casts it first.)
$apiMock->shouldNotReceive('getCommunityId');
$apiMock->shouldReceive('syncChannelPosts')
->once()
->with('token', 42, $channel->name);
$logSaverMock = Mockery::mock(LogSaver::class);
$logSaverMock->shouldReceive('info')->zeroOrMoreTimes();
$logSaverMock->shouldReceive('error')->zeroOrMoreTimes();
$this->makeJobWithApi($channel, $apiMock)->handle($logSaverMock);
// As above: the shouldNotReceive('getCommunityId') expectation is the assertion.
$this->addToAssertionCount(1);
}
/**
* @return array{0: PlatformChannel, 1: PlatformAccount}
*/
private function makeSyncableChannel(string $channelId): array
{
$platformInstance = PlatformInstance::factory()->create([
'platform' => PlatformEnum::LEMMY,
'url' => 'https://lemmy.example.com',
]);
$account = PlatformAccount::factory()->create([
'instance_url' => $platformInstance->url,
'is_active' => true,
]);
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id,
'name' => 'tech_news',
'channel_id' => $channelId,
'is_active' => true,
]);
$channel->platformAccounts()->attach($account->id, [
'is_active' => true,
'priority' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
return [$channel->fresh(['platformInstance']), $account];
}
private function makeJobWithApi(PlatformChannel $channel, LemmyApiService $api): SyncChannelPostsJob
{
return new class($channel, $api) extends SyncChannelPostsJob
{
public function __construct(PlatformChannel $channel, private readonly LemmyApiService $api)
{
parent::__construct($channel);
}
protected function makeApiService(string $instanceUrl): LemmyApiService
{
return $this->api;
}
};
}
public function test_job_can_be_serialized(): void
{
$platformInstance = PlatformInstance::factory()->create();
$channel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id,
'name' => 'Test Channel',
]);
$job = new SyncChannelPostsJob($channel);
$serialized = serialize($job);
$unserialized = unserialize($serialized);
$this->assertInstanceOf(SyncChannelPostsJob::class, $unserialized);
$this->assertEquals($job->queue, $unserialized->queue);
// Note: Cannot test channel property directly as it's private
// but serialization/unserialization working proves the job structure is intact
}
public function test_dispatch_for_all_active_channels_method_exists(): void
{
$this->assertTrue(method_exists(SyncChannelPostsJob::class, 'dispatchForAllActiveChannels'));
}
public function test_job_has_handle_method(): void
{
$channel = PlatformChannel::factory()->make();
$job = new SyncChannelPostsJob($channel);
$this->assertTrue(method_exists($job, 'handle'));
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
}