fix(social): publish flow honors PlatformUnavailable too
The previous commits in this PR closed the loophole on the hourly / daily token-refresh jobs. The same loophole remained on the publish path: every per-platform publisher (LinkedIn, X, YouTube, TikTok, Threads, Instagram, Pinterest, Bluesky and their Analytics siblings) has its own refreshToken() called before publishing a scheduled post, and all of those treated any non-2xx as TokenExpired — including 5xx. Result before this commit: a Bluesky outage that coincided with a scheduled publish would mark the account as expired and fail the post. Changes: - Route every refreshToken() in the 16 publisher / analytics classes through TokenRefreshClient::for(Platform::X)->send(...). - TokenRefreshClient now also fills platformErrorCode from the HTTP status and pulls error_description / error.message from the JSON body, preserving the richer info LinkedIn / X / TikTok / Pinterest / Threads used to put on their TokenExpiredException. - PublishToSocialPlatform catches PlatformUnavailableException explicitly: the post is marked failed (category: platform_unavailable, with http_status in error_context) but the account stays Connected. No retry inside this job — the scheduler reattempts the next run. Test added: publish flow does NOT mark account expired when the publisher throws PlatformUnavailable. Full suite: 1569 passing.
This commit is contained in:
parent
090cc761dd
commit
32e1f89adb
20 changed files with 144 additions and 192 deletions
|
|
@ -10,6 +10,7 @@
|
|||
use App\Enums\SocialAccount\Platform as SocialPlatform;
|
||||
use App\Enums\SocialAccount\Status;
|
||||
use App\Events\PostPlatformStatusUpdated;
|
||||
use App\Exceptions\PlatformUnavailableException;
|
||||
use App\Exceptions\Social\SocialPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Mail\PostPublished;
|
||||
|
|
@ -110,6 +111,19 @@ public function handle(): void
|
|||
$result = $publisher->publish($this->postPlatform);
|
||||
$this->postPlatform->markAsPublished(data_get($result, 'id'), data_get($result, 'url'));
|
||||
break;
|
||||
} catch (PlatformUnavailableException $e) {
|
||||
Log::warning('Publish skipped: platform unavailable', [
|
||||
'post_platform_id' => $this->postPlatform->id,
|
||||
'platform' => $this->postPlatform->platform->value,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$this->postPlatform->markAsFailed($e->getMessage(), [
|
||||
'category' => 'platform_unavailable',
|
||||
'http_status' => $e->httpStatus,
|
||||
'failed_at' => now()->toIso8601String(),
|
||||
]);
|
||||
break;
|
||||
} catch (TokenExpiredException $e) {
|
||||
if ($attempt < $maxAttempts) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -271,12 +271,12 @@ private function buildPostUrl(string $handle, string $postId): string
|
|||
public function refreshToken(SocialAccount $account): void
|
||||
{
|
||||
$service = $account->meta['service'] ?? config('trypost.platforms.bluesky.default_service');
|
||||
$client = TokenRefreshClient::for(Platform::Bluesky);
|
||||
|
||||
// Try refresh first
|
||||
$response = $this->socialHttp()->withToken($account->refresh_token)
|
||||
->post("{$service}/xrpc/com.atproto.server.refreshSession");
|
||||
try {
|
||||
$response = $client->send(fn () => $this->socialHttp()->withToken($account->refresh_token)
|
||||
->post("{$service}/xrpc/com.atproto.server.refreshSession"));
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
$account->update([
|
||||
'access_token' => data_get($data, 'accessJwt'),
|
||||
|
|
@ -285,37 +285,27 @@ public function refreshToken(SocialAccount $account): void
|
|||
]);
|
||||
|
||||
return;
|
||||
} catch (TokenExpiredException) {
|
||||
// refresh token rejected (4xx) — fall back to re-auth below
|
||||
}
|
||||
|
||||
Log::warning('Bluesky refresh token failed, trying re-authentication', [
|
||||
'status' => $response->status(),
|
||||
]);
|
||||
|
||||
// If refresh fails, re-authenticate with stored credentials
|
||||
if (isset($account->meta['password'])) {
|
||||
try {
|
||||
$password = decrypt($account->meta['password']);
|
||||
$identifier = $account->meta['identifier'];
|
||||
$reauth = $client->send(fn () => Http::post("{$service}/xrpc/com.atproto.server.createSession", [
|
||||
'identifier' => $account->meta['identifier'],
|
||||
'password' => decrypt($account->meta['password']),
|
||||
]));
|
||||
|
||||
$response = Http::post("{$service}/xrpc/com.atproto.server.createSession", [
|
||||
'identifier' => $identifier,
|
||||
'password' => $password,
|
||||
$data = $reauth->json();
|
||||
$account->update([
|
||||
'access_token' => data_get($data, 'accessJwt'),
|
||||
'refresh_token' => data_get($data, 'refreshJwt'),
|
||||
'token_expires_at' => now()->addHours(2),
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
$account->update([
|
||||
'access_token' => data_get($data, 'accessJwt'),
|
||||
'refresh_token' => data_get($data, 'refreshJwt'),
|
||||
'token_expires_at' => now()->addHours(2),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bluesky re-authentication failed', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return;
|
||||
} catch (TokenExpiredException) {
|
||||
// re-auth rejected with stored credentials — fall through
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -211,15 +211,10 @@ private function refreshToken(SocialAccount $account): void
|
|||
throw new TokenExpiredException('No refresh token available for Instagram account');
|
||||
}
|
||||
|
||||
$response = Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [
|
||||
$response = TokenRefreshClient::for(Platform::Instagram)->send(fn () => Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [
|
||||
'grant_type' => 'ig_refresh_token',
|
||||
'access_token' => $account->access_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('Instagram token refresh failed', ['body' => $this->redactResponseBody($response->body())]);
|
||||
throw new TokenExpiredException('Instagram token refresh failed');
|
||||
}
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
use App\Exceptions\Social\InstagramPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Services\Media\MediaOptimizer;
|
||||
|
|
@ -403,16 +402,10 @@ private function refreshToken(SocialAccount $account): void
|
|||
return;
|
||||
}
|
||||
|
||||
$response = Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [
|
||||
$response = TokenRefreshClient::for(Platform::Instagram)->send(fn () => Http::get(config('trypost.platforms.instagram.auth_api').'/refresh_access_token', [
|
||||
'grant_type' => 'ig_refresh_token',
|
||||
'access_token' => $account->access_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('Instagram token refresh failed', ['body' => $this->redactResponseBody($response->body())]);
|
||||
|
||||
throw new TokenExpiredException('Failed to refresh Instagram token');
|
||||
}
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
$newToken = data_get($data, 'access_token');
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
|
|
@ -240,17 +241,13 @@ private function refreshToken(SocialAccount $account): void
|
|||
throw new TokenExpiredException('No refresh token available for LinkedIn Page account');
|
||||
}
|
||||
|
||||
$response = Http::asForm()->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
'client_id' => config('services.linkedin-openid.client_id'),
|
||||
'client_secret' => config('services.linkedin-openid.client_secret'),
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('LinkedIn token refresh failed', ['body' => $this->redactResponseBody($response->body())]);
|
||||
throw new TokenExpiredException('LinkedIn token refresh failed');
|
||||
}
|
||||
$response = TokenRefreshClient::for(Platform::LinkedInPage)->send(fn () => Http::asForm()
|
||||
->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
'client_id' => config('services.linkedin-openid.client_id'),
|
||||
'client_secret' => config('services.linkedin-openid.client_secret'),
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -458,19 +458,13 @@ private function refreshToken(SocialAccount $account): void
|
|||
throw new TokenExpiredException('No refresh token available for LinkedIn Page account');
|
||||
}
|
||||
|
||||
$response = Http::asForm()->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
'client_id' => config('services.linkedin-openid.client_id'),
|
||||
'client_secret' => config('services.linkedin-openid.client_secret'),
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new TokenExpiredException(
|
||||
message: data_get($response->json(), 'error_description', 'Failed to refresh LinkedIn Page token'),
|
||||
platformErrorCode: (string) $response->status(),
|
||||
);
|
||||
}
|
||||
$response = TokenRefreshClient::for(Platform::LinkedInPage)->send(fn () => Http::asForm()
|
||||
->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
'client_id' => config('services.linkedin-openid.client_id'),
|
||||
'client_secret' => config('services.linkedin-openid.client_secret'),
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -439,19 +439,13 @@ private function refreshToken(SocialAccount $account): void
|
|||
throw new TokenExpiredException('No refresh token available for LinkedIn account');
|
||||
}
|
||||
|
||||
$response = Http::asForm()->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
'client_id' => config('services.linkedin.client_id'),
|
||||
'client_secret' => config('services.linkedin.client_secret'),
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new TokenExpiredException(
|
||||
message: data_get($response->json(), 'error_description', 'Failed to refresh LinkedIn token'),
|
||||
platformErrorCode: (string) $response->status(),
|
||||
);
|
||||
}
|
||||
$response = TokenRefreshClient::for(Platform::LinkedIn)->send(fn () => Http::asForm()
|
||||
->post(config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
'client_id' => config('services.linkedin.client_id'),
|
||||
'client_secret' => config('services.linkedin.client_secret'),
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
|
|
@ -167,18 +168,13 @@ private function refreshToken(SocialAccount $account): void
|
|||
throw new TokenExpiredException('No refresh token available for Pinterest account');
|
||||
}
|
||||
|
||||
$response = Http::withBasicAuth(
|
||||
$response = TokenRefreshClient::for(Platform::Pinterest)->send(fn () => Http::withBasicAuth(
|
||||
config('services.pinterest.client_id'),
|
||||
config('services.pinterest.client_secret'),
|
||||
)->asForm()->post(config('trypost.platforms.pinterest.api').'/oauth/token', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('Pinterest token refresh failed', ['body' => $this->redactResponseBody($response->body())]);
|
||||
throw new TokenExpiredException('Pinterest token refresh failed');
|
||||
}
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
use App\Exceptions\Social\PinterestPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Services\Media\MediaOptimizer;
|
||||
|
|
@ -403,7 +402,7 @@ private function waitForMediaProcessing(SocialAccount $account, string $mediaId,
|
|||
|
||||
private function refreshToken(SocialAccount $account): void
|
||||
{
|
||||
$response = Http::asForm()
|
||||
$response = TokenRefreshClient::for(Platform::Pinterest)->send(fn () => Http::asForm()
|
||||
->withBasicAuth(
|
||||
config('services.pinterest.client_id'),
|
||||
config('services.pinterest.client_secret')
|
||||
|
|
@ -411,14 +410,7 @@ private function refreshToken(SocialAccount $account): void
|
|||
->post($this->baseUrl.'/oauth/token', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new TokenExpiredException(
|
||||
message: data_get($response->json(), 'error_description', 'Failed to refresh Pinterest token'),
|
||||
platformErrorCode: (string) $response->status(),
|
||||
);
|
||||
}
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Services\Social\Concerns\HasSocialHttpClient;
|
||||
|
|
@ -141,15 +141,10 @@ private function getHttpClient(): PendingRequest
|
|||
|
||||
private function refreshToken(SocialAccount $account): void
|
||||
{
|
||||
$response = Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [
|
||||
$response = TokenRefreshClient::for(Platform::Threads)->send(fn () => Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [
|
||||
'grant_type' => 'th_refresh_token',
|
||||
'access_token' => $account->access_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('Threads token refresh failed', ['body' => $this->redactResponseBody($response->body())]);
|
||||
throw new TokenExpiredException('Threads token refresh failed');
|
||||
}
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\ThreadsPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Services\Social\Concerns\HasSocialHttpClient;
|
||||
|
|
@ -306,17 +306,10 @@ private function waitForMediaProcessing(string $containerId, string $accessToken
|
|||
private function refreshToken(SocialAccount $account): void
|
||||
{
|
||||
// Threads uses long-lived tokens that can be refreshed
|
||||
$response = Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [
|
||||
$response = TokenRefreshClient::for(Platform::Threads)->send(fn () => Http::get(config('trypost.platforms.threads.auth_api').'/refresh_access_token', [
|
||||
'grant_type' => 'th_refresh_token',
|
||||
'access_token' => $account->access_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new TokenExpiredException(
|
||||
message: data_get($response->json(), 'error.message', 'Failed to refresh Threads token'),
|
||||
platformErrorCode: (string) $response->status(),
|
||||
);
|
||||
}
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Services\Social\Concerns\HasSocialHttpClient;
|
||||
|
|
@ -166,17 +167,13 @@ private function refreshToken(SocialAccount $account): void
|
|||
throw new TokenExpiredException('No refresh token available for TikTok account');
|
||||
}
|
||||
|
||||
$response = Http::asForm()->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [
|
||||
'client_key' => config('services.tiktok.client_id'),
|
||||
'client_secret' => config('services.tiktok.client_secret'),
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('TikTok token refresh failed', ['body' => $this->redactResponseBody($response->body())]);
|
||||
throw new TokenExpiredException('TikTok token refresh failed');
|
||||
}
|
||||
$response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm()
|
||||
->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [
|
||||
'client_key' => config('services.tiktok.client_id'),
|
||||
'client_secret' => config('services.tiktok.client_secret'),
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Services\Social\Concerns\HasSocialHttpClient;
|
||||
|
|
@ -130,17 +131,13 @@ private function refreshToken(SocialAccount $account): void
|
|||
throw new TokenExpiredException('No refresh token available for TikTok account');
|
||||
}
|
||||
|
||||
$response = Http::asForm()->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [
|
||||
'client_key' => config('services.tiktok.client_id'),
|
||||
'client_secret' => config('services.tiktok.client_secret'),
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('TikTok token refresh failed', ['body' => $this->redactResponseBody($response->body())]);
|
||||
throw new TokenExpiredException('TikTok token refresh failed');
|
||||
}
|
||||
$response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm()
|
||||
->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [
|
||||
'client_key' => config('services.tiktok.client_id'),
|
||||
'client_secret' => config('services.tiktok.client_secret'),
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
use App\Exceptions\Social\TikTokPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
|
|
@ -322,19 +323,13 @@ private function refreshToken(SocialAccount $account): void
|
|||
throw new TokenExpiredException('No refresh token available for TikTok account');
|
||||
}
|
||||
|
||||
$response = Http::asForm()->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [
|
||||
'client_key' => config('services.tiktok.client_id'),
|
||||
'client_secret' => config('services.tiktok.client_secret'),
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new TokenExpiredException(
|
||||
message: data_get($response->json(), 'error.message', 'Failed to refresh TikTok token'),
|
||||
platformErrorCode: (string) $response->status(),
|
||||
);
|
||||
}
|
||||
$response = TokenRefreshClient::for(Platform::TikTok)->send(fn () => Http::asForm()
|
||||
->post(config('trypost.platforms.tiktok.api').'/oauth/token/', [
|
||||
'client_key' => config('services.tiktok.client_id'),
|
||||
'client_secret' => config('services.tiktok.client_secret'),
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,13 @@ public function send(Closure $request): Response
|
|||
Log::error("TokenRefreshClient: {$name} token refresh failed", [
|
||||
'body' => $this->redactBody($response->body()),
|
||||
]);
|
||||
throw new TokenExpiredException("Failed to refresh {$name} token");
|
||||
|
||||
$body = $response->json();
|
||||
$message = data_get($body, 'error_description')
|
||||
?? data_get($body, 'error.message')
|
||||
?? "Failed to refresh {$name} token";
|
||||
|
||||
throw new TokenExpiredException($message, platformErrorCode: (string) $response->status());
|
||||
}
|
||||
|
||||
return $response;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
|
|
@ -206,18 +207,13 @@ private function refreshToken(SocialAccount $account): void
|
|||
throw new TokenExpiredException('No refresh token available for X account');
|
||||
}
|
||||
|
||||
$response = $this->socialHttp()
|
||||
$response = TokenRefreshClient::for(Platform::X)->send(fn () => $this->socialHttp()
|
||||
->withBasicAuth(config('services.x.client_id'), config('services.x.client_secret'))
|
||||
->asForm()
|
||||
->post(config('trypost.platforms.x.api').'/oauth2/token', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('X token refresh failed', ['body' => $this->redactResponseBody($response->body())]);
|
||||
throw new TokenExpiredException('X token refresh failed');
|
||||
}
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -335,19 +335,12 @@ private function refreshToken(SocialAccount $account): void
|
|||
throw new TokenExpiredException('No refresh token available for X account');
|
||||
}
|
||||
|
||||
$response = Http::asForm()
|
||||
$response = TokenRefreshClient::for(Platform::X)->send(fn () => Http::asForm()
|
||||
->withBasicAuth(config('services.x.client_id'), config('services.x.client_secret'))
|
||||
->post("{$this->baseUrl}/oauth2/token", [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new TokenExpiredException(
|
||||
message: data_get($response->json(), 'error_description', 'Failed to refresh X token'),
|
||||
platformErrorCode: (string) $response->status(),
|
||||
);
|
||||
}
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
|
|
@ -168,18 +169,13 @@ private function refreshToken(SocialAccount $account): void
|
|||
throw new TokenExpiredException('No refresh token available for YouTube account');
|
||||
}
|
||||
|
||||
$response = Http::asForm()->post(config('trypost.platforms.youtube.oauth_api').'/token', [
|
||||
'client_id' => config('services.google.client_id'),
|
||||
'client_secret' => config('services.google.client_secret'),
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('YouTube token refresh failed', ['body' => $this->redactResponseBody($response->body())]);
|
||||
|
||||
throw new TokenExpiredException('Failed to refresh YouTube token');
|
||||
}
|
||||
$response = TokenRefreshClient::for(Platform::YouTube)->send(fn () => Http::asForm()
|
||||
->post(config('trypost.platforms.youtube.oauth_api').'/token', [
|
||||
'client_id' => config('services.google.client_id'),
|
||||
'client_secret' => config('services.google.client_secret'),
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
use App\Exceptions\Social\YouTubePublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
|
|
@ -229,18 +230,13 @@ private function refreshToken(SocialAccount $account): void
|
|||
throw new TokenExpiredException('No refresh token available for YouTube account');
|
||||
}
|
||||
|
||||
$response = Http::asForm()->post(config('trypost.platforms.youtube.oauth_api').'/token', [
|
||||
'client_id' => config('services.google.client_id'),
|
||||
'client_secret' => config('services.google.client_secret'),
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('YouTube token refresh failed', ['body' => $this->redactResponseBody($response->body())]);
|
||||
|
||||
throw new TokenExpiredException('Failed to refresh YouTube token');
|
||||
}
|
||||
$response = TokenRefreshClient::for(Platform::YouTube)->send(fn () => Http::asForm()
|
||||
->post(config('trypost.platforms.youtube.oauth_api').'/token', [
|
||||
'client_id' => config('services.google.client_id'),
|
||||
'client_secret' => config('services.google.client_secret'),
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $account->refresh_token,
|
||||
]));
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
use App\Events\PostPlatformStatusUpdated;
|
||||
use App\Exceptions\Social\ErrorCategory;
|
||||
use App\Exceptions\Social\LinkedInPublishException;
|
||||
use App\Exceptions\PlatformUnavailableException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Jobs\PublishToSocialPlatform;
|
||||
use App\Jobs\SendNotification;
|
||||
|
|
@ -109,6 +110,28 @@
|
|||
expect($this->socialAccount->status)->toBe(AccountStatus::TokenExpired);
|
||||
});
|
||||
|
||||
test('publish to social platform does NOT mark account expired when platform is unavailable', function () {
|
||||
Event::fake();
|
||||
Mail::fake();
|
||||
|
||||
$publisher = Mockery::mock(LinkedInPublisher::class);
|
||||
$publisher->shouldReceive('publish')->andThrow(
|
||||
new PlatformUnavailableException('LinkedIn API returned 503 during token refresh', 503)
|
||||
);
|
||||
|
||||
$this->app->instance(LinkedInPublisher::class, $publisher);
|
||||
|
||||
(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 updates post status when all platforms finished', function () {
|
||||
Event::fake();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue