fix(social): move Mastodon default instance to config + cleanup

Review follow-ups:

- verifyMastodon was the last hardcoded host left after the PR moved
  LinkedIn/YouTube/Bluesky to config. Adds trypost.platforms.mastodon
  .default_instance (env MASTODON_DEFAULT_INSTANCE) and reads from it.
- refreshToken() docblock now declares @throws PlatformUnavailableException
  (the whole point of the PR was missing from its contract).
- Strip the new explanatory comments inside catch blocks and tests —
  rationale lives in the commit / PR, not inline. The two comments
  inside empty `catch (TokenExpiredException) {}` blocks stay because
  there the comment is the only thing telling the reader why the
  exception is swallowed.
This commit is contained in:
Paulo Castellano 2026-05-19 08:27:46 -03:00
parent 6f96d67dbc
commit d5e28e3d02
7 changed files with 6 additions and 22 deletions

View file

@ -26,20 +26,12 @@ public function handle(ConnectionVerifier $verifier): void
try {
$verifier->refreshToken($this->account);
} catch (PlatformUnavailableException $e) {
// Platform is down (5xx / network). Leave the account alone —
// next scheduled run will try again. Critically, do NOT mark
// the account expired: that would trigger a false-positive
// "reconnect your account" notification.
Log::warning('Token refresh skipped: platform unavailable', [
'account_id' => $this->account->id,
'platform' => $this->account->platform->value,
'error' => $e->getMessage(),
]);
} catch (TokenExpiredException $e) {
// refresh_token rejected by the provider (revoked / rotated /
// expired beyond refresh). Mark the account so the user is
// notified immediately instead of waiting for the next failed
// publish or the daily verify pass.
$this->account->markAsTokenExpired($e->getMessage());
} catch (Throwable $e) {
Log::warning('Proactive token refresh failed', [

View file

@ -66,8 +66,6 @@ private function verifyAccount(ConnectionVerifier $verifier, SocialAccount $acco
return true;
} catch (PlatformUnavailableException $e) {
// Platform is down (5xx / network). The account's token is not
// provably bad — skip silently and try again next pass.
Log::warning('Social account verification skipped: platform unavailable', [
'account_id' => $account->id,
'platform' => $account->platform->value,

View file

@ -28,9 +28,6 @@ public function verify(SocialAccount $account): bool
// LinkedIn, etc.) invalidate the previous refresh_token on each
// refresh, so proactive refreshes during races cause false-positive
// disconnects even though the access_token still works fine.
//
// PlatformUnavailableException from refresh propagates naturally so
// the caller can distinguish "platform is down" from "token bad".
if ($account->is_token_expired) {
$this->refreshToken($account);
@ -76,9 +73,10 @@ private function callVerifyEndpoint(SocialAccount $account): bool
* Refresh the account's token via the platform-specific OAuth flow.
* Callers that want the smart "try access_token first" behavior should
* use verify() instead. This method always attempts a refresh under
* the per-account lock and throws TokenExpiredException on failure.
* the per-account lock.
*
* @throws TokenExpiredException if refresh fails
* @throws TokenExpiredException if refresh is rejected by the provider (4xx)
* @throws PlatformUnavailableException if the platform is unreachable (5xx / network)
*/
public function refreshToken(SocialAccount $account): void
{
@ -168,9 +166,6 @@ private function refreshBlueskyToken(SocialAccount $account): void
$service = $account->meta['service'] ?? config('trypost.platforms.bluesky.default_service');
$client = TokenRefreshClient::for(Platform::Bluesky);
// Try refresh token first. Connection errors / 5xx surface as
// PlatformUnavailableException (Bluesky is down — don't touch the
// account's status). 4xx falls through to the re-auth fallback.
try {
$response = $client->send(fn () => Http::withToken($account->refresh_token)
->post("{$service}/xrpc/com.atproto.server.refreshSession"));
@ -512,7 +507,7 @@ private function verifyBluesky(SocialAccount $account): bool
private function verifyMastodon(SocialAccount $account): bool
{
$instance = $account->meta['instance'] ?? 'https://mastodon.social';
$instance = $account->meta['instance'] ?? config('trypost.platforms.mastodon.default_instance');
$response = Http::withToken($account->access_token)
->get("{$instance}/api/v1/accounts/verify_credentials");

View file

@ -116,6 +116,8 @@
],
'mastodon' => [
'enabled' => env('MASTODON_ENABLED', true),
// Default instance used when the account has no `meta.instance` override.
'default_instance' => env('MASTODON_DEFAULT_INSTANCE', 'https://mastodon.social'),
],
],

View file

@ -86,7 +86,6 @@
(new RefreshSocialToken($this->account))->handle($verifier);
// Critically: account status stays Connected, no notification dispatched.
expect($this->account->fresh()->status)->toBe(Status::Connected);
Queue::assertNotPushed(SendNotification::class);
});

View file

@ -411,7 +411,6 @@
test('bluesky 5xx during refresh raises PlatformUnavailable even when password fallback is stored', function () {
Http::fake([
// Both endpoints return 5xx — the platform is genuinely down.
'bsky.social/xrpc/com.atproto.server.refreshSession' => Http::response('upstream timeout', 503),
'bsky.social/xrpc/com.atproto.server.createSession' => Http::response('upstream timeout', 503),
]);

View file

@ -137,7 +137,6 @@
VerifyWorkspaceConnections::dispatch($workspace);
// Status untouched, no notification email.
expect($account->fresh()->status)->toBe(Status::Connected);
Mail::assertNothingQueued();
});