shouldReceive('login')->andReturn($loginSucceeds ? 'a-token' : null); $job = new class($api) extends CheckPlatformCredentialsJob { public function __construct(private LemmyApiService $api) {} protected function makeApiService(PlatformAccount $account): LemmyApiService { return $this->api; } }; $job->handle(app(NotificationService::class)); } /** * @param array $attributes */ private function account(array $attributes = []): PlatformAccount { return PlatformAccount::factory()->create(array_merge(['is_active' => true], $attributes)); } public function test_a_successful_login_marks_the_account_healthy(): void { $account = $this->account(['status' => AccountStatusEnum::UNTESTED]); $this->runJob(true); $account->refresh(); $this->assertSame(AccountStatusEnum::HEALTHY, $account->status); $this->assertSame(0, $account->consecutive_failures); $this->assertNotNull($account->last_tested_at); } public function test_a_successful_login_clears_earlier_failures(): void { $account = $this->account(['status' => AccountStatusEnum::UNHEALTHY, 'consecutive_failures' => 2]); $this->runJob(true); $account->refresh(); $this->assertSame(AccountStatusEnum::HEALTHY, $account->status); $this->assertSame(0, $account->consecutive_failures); } public function test_a_single_failure_does_not_mark_the_account_unhealthy(): void { $account = $this->account(['status' => AccountStatusEnum::HEALTHY]); $this->runJob(false); $account->refresh(); $this->assertSame(AccountStatusEnum::HEALTHY, $account->status); $this->assertSame(1, $account->consecutive_failures); } public function test_the_threshold_failure_marks_the_account_unhealthy(): void { $account = $this->account([ 'status' => AccountStatusEnum::HEALTHY, 'consecutive_failures' => PlatformAccount::FAILURES_BEFORE_UNHEALTHY - 1, ]); $this->runJob(false); $this->assertSame(AccountStatusEnum::UNHEALTHY, $account->refresh()->status); } public function test_a_single_failure_does_not_notify(): void { $this->account(['status' => AccountStatusEnum::HEALTHY]); $this->runJob(false); $this->assertDatabaseCount('notifications', 0); } public function test_becoming_unhealthy_notifies(): void { $account = $this->account([ 'status' => AccountStatusEnum::HEALTHY, 'consecutive_failures' => PlatformAccount::FAILURES_BEFORE_UNHEALTHY - 1, 'username' => 'newsbot', ]); $this->runJob(false); $this->assertDatabaseHas('notifications', [ 'type' => NotificationTypeEnum::CREDENTIAL_EXPIRED->value, 'notifiable_type' => $account->getMorphClass(), 'notifiable_id' => $account->id, ]); $this->assertStringContainsString('newsbot', Notification::first()->title); } public function test_an_account_already_unhealthy_does_not_notify_again(): void { $this->account([ 'status' => AccountStatusEnum::UNHEALTHY, 'consecutive_failures' => PlatformAccount::FAILURES_BEFORE_UNHEALTHY, ]); $this->runJob(false); $this->assertDatabaseCount('notifications', 0); } public function test_inactive_accounts_are_not_checked(): void { $account = $this->account(['is_active' => false, 'status' => AccountStatusEnum::UNTESTED]); $this->runJob(false); $account->refresh(); $this->assertSame(AccountStatusEnum::UNTESTED, $account->status); $this->assertNull($account->last_tested_at); } public function test_an_exception_during_login_counts_as_a_failure(): void { $account = $this->account(['status' => AccountStatusEnum::HEALTHY]); $api = Mockery::mock(LemmyApiService::class); $api->shouldReceive('login')->andThrow(new \RuntimeException('instance unreachable')); $job = new class($api) extends CheckPlatformCredentialsJob { public function __construct(private LemmyApiService $api) {} protected function makeApiService(PlatformAccount $account): LemmyApiService { return $this->api; } }; $job->handle(app(NotificationService::class)); $this->assertSame(1, $account->refresh()->consecutive_failures); } }