trypost/tests/Feature/Jobs/RefreshSocialTokenTest.php
Paulo Castellano d5e28e3d02 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.
2026-05-19 08:27:46 -03:00

91 lines
3.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Enums\SocialAccount\Status;
use App\Exceptions\PlatformUnavailableException;
use App\Exceptions\TokenExpiredException;
use App\Jobs\RefreshSocialToken;
use App\Jobs\SendNotification;
use App\Models\SocialAccount;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Social\ConnectionVerifier;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
beforeEach(function () {
$this->owner = User::factory()->create();
$this->workspace = Workspace::factory()->create(['user_id' => $this->owner->id]);
$this->account = SocialAccount::factory()->x()->create([
'workspace_id' => $this->workspace->id,
'status' => Status::Connected,
'username' => 'testuser',
]);
});
test('refresh job calls refreshToken (not verify) on the verifier', function () {
$verifier = mock(ConnectionVerifier::class);
$verifier->shouldReceive('refreshToken')->once()->with(
Mockery::on(fn ($account) => $account->id === $this->account->id)
);
$verifier->shouldNotReceive('verify');
app()->instance(ConnectionVerifier::class, $verifier);
(new RefreshSocialToken($this->account))->handle($verifier);
});
test('refresh job marks account as TokenExpired when refresh_token is rejected', function () {
Queue::fake();
$verifier = mock(ConnectionVerifier::class);
$verifier->shouldReceive('refreshToken')->once()->andThrow(
new TokenExpiredException('refresh_token revoked')
);
app()->instance(ConnectionVerifier::class, $verifier);
(new RefreshSocialToken($this->account))->handle($verifier);
expect($this->account->fresh()->status)->toBe(Status::TokenExpired);
expect($this->account->fresh()->error_message)->toBe('refresh_token revoked');
// Notification dispatched because account transitioned from Connected.
Queue::assertPushed(SendNotification::class);
});
test('refresh job logs warning on non-token errors and leaves status alone', function () {
Log::shouldReceive('warning')->once()->withArgs(function ($message, $context) {
return $message === 'Proactive token refresh failed'
&& $context['account_id'] === $this->account->id
&& $context['error'] === 'network blip';
});
$verifier = mock(ConnectionVerifier::class);
$verifier->shouldReceive('refreshToken')->once()->andThrow(new RuntimeException('network blip'));
app()->instance(ConnectionVerifier::class, $verifier);
(new RefreshSocialToken($this->account))->handle($verifier);
expect($this->account->fresh()->status)->toBe(Status::Connected);
});
test('refresh job does NOT mark account expired when platform is unavailable', function () {
Queue::fake();
Log::shouldReceive('warning')->once()->withArgs(function ($message, $context) {
return $message === 'Token refresh skipped: platform unavailable'
&& $context['account_id'] === $this->account->id
&& str_contains($context['error'], '503');
});
$verifier = mock(ConnectionVerifier::class);
$verifier->shouldReceive('refreshToken')->once()->andThrow(
new PlatformUnavailableException('X API returned 503 during token refresh', 503)
);
app()->instance(ConnectionVerifier::class, $verifier);
(new RefreshSocialToken($this->account))->handle($verifier);
expect($this->account->fresh()->status)->toBe(Status::Connected);
Queue::assertNotPushed(SendNotification::class);
});