From a42962c0cabdce3cd492b6f48113f9de0576d151 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 09:47:12 -0300 Subject: [PATCH] fix(social): publish retry honors PlatformUnavailable too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Edge case from the prior commits: if the publisher throws TokenExpired (401 path), PublishToSocialPlatform attempts refreshAccountToken() to recover. That internally goes through ConnectionVerifier::verify, which can now raise PlatformUnavailable (5xx). The old catch (\Throwable) swallowed it but the loop still fell through to markAsTokenExpired — meaning a transient platform outage during a retry could still flip the account to expired. Adds an explicit PlatformUnavailable catch in the retry block: marks the post failed with category platform_unavailable and breaks before touching the account status. --- app/Jobs/PublishToSocialPlatform.php | 13 ++++++++ .../Jobs/PublishToSocialPlatformTest.php | 30 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/app/Jobs/PublishToSocialPlatform.php b/app/Jobs/PublishToSocialPlatform.php index 0ee2b719..b37c40d1 100644 --- a/app/Jobs/PublishToSocialPlatform.php +++ b/app/Jobs/PublishToSocialPlatform.php @@ -130,6 +130,19 @@ public function handle(): void $this->refreshAccountToken(); continue; + } catch (PlatformUnavailableException $refreshError) { + Log::warning('Publish skipped: platform unavailable during retry refresh', [ + 'post_platform_id' => $this->postPlatform->id, + 'platform' => $this->postPlatform->platform->value, + 'error' => $refreshError->getMessage(), + ]); + + $this->postPlatform->markAsFailed($refreshError->getMessage(), [ + 'category' => 'platform_unavailable', + 'http_status' => $refreshError->httpStatus, + 'failed_at' => now()->toIso8601String(), + ]); + break; } catch (\Throwable $refreshError) { Log::error('Token refresh failed during publish retry', [ 'post_platform_id' => $this->postPlatform->id, diff --git a/tests/Feature/Jobs/PublishToSocialPlatformTest.php b/tests/Feature/Jobs/PublishToSocialPlatformTest.php index 30d4c175..ecedf7af 100644 --- a/tests/Feature/Jobs/PublishToSocialPlatformTest.php +++ b/tests/Feature/Jobs/PublishToSocialPlatformTest.php @@ -110,6 +110,36 @@ expect($this->socialAccount->status)->toBe(AccountStatus::TokenExpired); }); +test('publish does NOT mark account expired when retry refresh hits platform unavailable', function () { + Event::fake(); + Mail::fake(); + + // Publisher first throws TokenExpired (401-style), the retry-refresh + // path goes through ConnectionVerifier::verify which can in turn raise + // PlatformUnavailable if the platform is down. The account must stay + // Connected — it was the platform that failed, not the token. + $publisher = Mockery::mock(LinkedInPublisher::class); + $publisher->shouldReceive('publish')->andThrow(new TokenExpiredException('Token expired', '401')); + + $verifier = Mockery::mock(ConnectionVerifier::class); + $verifier->shouldReceive('verify')->andThrow( + new PlatformUnavailableException('LinkedIn API returned 503 during token refresh', 503) + ); + + $this->app->instance(LinkedInPublisher::class, $publisher); + $this->app->instance(ConnectionVerifier::class, $verifier); + + (new PublishToSocialPlatform($this->postPlatform))->handle(); + + $this->postPlatform->refresh(); + $this->socialAccount->refresh(); + + expect($this->postPlatform->status)->toBe(PlatformStatus::Failed); + expect($this->postPlatform->error_context['category'] ?? null)->toBe('platform_unavailable'); + expect($this->postPlatform->error_context['http_status'] ?? null)->toBe(503); + expect($this->socialAccount->status)->toBe(AccountStatus::Connected); +}); + test('publish to social platform does NOT mark account expired when platform is unavailable', function () { Event::fake(); Mail::fake();