test(social): tests pull OAuth URLs from config

Same rule applied to production code in earlier commits now applies to
tests: Http::fake patterns and assertions read from
config('trypost.platforms.*.oauth_api' / '.api' / '.default_service')
instead of hardcoded strings. Hardcoded URLs in tests drift silently
when the config changes.

Also documents the rule in CLAUDE.md under "External Service URLs" so
new code (and tests) start in the right place — only the host comes
from config, path/RPC segments stay inline next to the call.
This commit is contained in:
Paulo Castellano 2026-05-19 09:37:16 -03:00
parent f975171a9a
commit 04975020e4
3 changed files with 25 additions and 14 deletions

View file

@ -302,6 +302,13 @@ ## String Interpolation
- Use curly braces `{}` even for simple variables to keep the boundary explicit and to allow object/array access without ambiguity.
- Single quotes are still preferred when the string has no interpolation.
## External Service URLs
- NEVER hardcode third-party API hosts, OAuth endpoints, or per-platform service URLs (e.g. `https://api.x.com/2`, `https://www.linkedin.com/oauth/v2/accessToken`, `https://bsky.social`). They live in `config/trypost.php` under `platforms.<name>` with a matching `env(...)` default, so self-hosted users can override them and we have a single source of truth.
- Production code: `config('trypost.platforms.linkedin.oauth_api').'/oauth/v2/accessToken'`, never the literal URL.
- Tests: use the same `config(...)` value in `Http::fake([...])``Http::fake([config('trypost.platforms.x.api').'/oauth2/token' => ...])`. Tests with hardcoded URLs drift silently when the config changes.
- Path/route segments after the host (e.g. `/oauth/v2/accessToken`, `/xrpc/com.atproto.server.refreshSession`) are part of the provider's protocol spec — those stay inline next to the call. Only the host comes from config.
## TryPost.it Documentation
- All our documentation to final user it's under https://docs.trypost.it

View file

@ -364,14 +364,16 @@
});
test('5xx during refresh raises PlatformUnavailableException, not TokenExpiredException', function () {
$service = config('trypost.platforms.bluesky.default_service');
Http::fake([
'bsky.social/xrpc/com.atproto.server.refreshSession' => Http::response('upstream timeout', 503),
"{$service}/xrpc/com.atproto.server.refreshSession" => Http::response('upstream timeout', 503),
]);
$account = SocialAccount::factory()->bluesky()->create([
'token_expires_at' => now()->subMinutes(5),
'refresh_token' => 'old_refresh_token',
'meta' => ['service' => 'https://bsky.social'],
'meta' => ['service' => $service],
]);
$verifier = new ConnectionVerifier;
@ -381,7 +383,7 @@
test('connection failure during refresh raises PlatformUnavailableException', function () {
Http::fake([
'oauth2.googleapis.com/token' => fn () => throw new ConnectionException('cURL error 7: connection refused'),
config('trypost.platforms.youtube.oauth_api').'/token' => fn () => throw new ConnectionException('cURL error 7: connection refused'),
]);
$account = SocialAccount::factory()->youtube()->create([
@ -396,7 +398,7 @@
test('4xx during refresh keeps raising TokenExpiredException', function () {
Http::fake([
'api.x.com/2/oauth2/token' => Http::response(['error' => 'invalid_grant'], 400),
config('trypost.platforms.x.api').'/oauth2/token' => Http::response(['error' => 'invalid_grant'], 400),
]);
$account = SocialAccount::factory()->x()->create([
@ -411,7 +413,7 @@
test('429 during refresh raises PlatformUnavailableException (rate limit is transient)', function () {
Http::fake([
'api.x.com/2/oauth2/token' => Http::response(['error' => 'rate_limit_exceeded'], 429),
config('trypost.platforms.x.api').'/oauth2/token' => Http::response(['error' => 'rate_limit_exceeded'], 429),
]);
$account = SocialAccount::factory()->x()->create([
@ -425,16 +427,18 @@
});
test('bluesky 5xx during refresh raises PlatformUnavailable even when password fallback is stored', function () {
$service = config('trypost.platforms.bluesky.default_service');
Http::fake([
'bsky.social/xrpc/com.atproto.server.refreshSession' => Http::response('upstream timeout', 503),
'bsky.social/xrpc/com.atproto.server.createSession' => Http::response('upstream timeout', 503),
"{$service}/xrpc/com.atproto.server.refreshSession" => Http::response('upstream timeout', 503),
"{$service}/xrpc/com.atproto.server.createSession" => Http::response('upstream timeout', 503),
]);
$account = SocialAccount::factory()->bluesky()->create([
'token_expires_at' => now()->subMinutes(5),
'refresh_token' => 'old_refresh_token',
'meta' => [
'service' => 'https://bsky.social',
'service' => $service,
'identifier' => 'user.bsky.social',
'password' => encrypt('app-password'),
],

View file

@ -34,13 +34,16 @@
});
test('linkedin page analytics refresh hits the configured oauth host', function () {
$oauthApi = config('trypost.platforms.linkedin.oauth_api');
$api = config('trypost.platforms.linkedin-page.api');
Http::fake([
'www.linkedin.com/oauth/v2/accessToken' => Http::response([
"{$oauthApi}/oauth/v2/accessToken" => Http::response([
'access_token' => 'new_token',
'refresh_token' => 'new_refresh_token',
'expires_in' => 5184000,
], 200),
'api.linkedin.com/rest/socialActions/*' => Http::response([
"{$api}/rest/socialActions/*" => Http::response([
'likesSummary' => ['totalLikes' => 0],
'commentsSummary' => ['aggregatedTotalComments' => 0],
], 200),
@ -48,8 +51,5 @@
(new LinkedInPageAnalytics)->fetchPostMetrics($this->postPlatform);
Http::assertSent(fn ($request) => str_contains(
$request->url(),
rtrim((string) config('trypost.platforms.linkedin.oauth_api'), '/').'/oauth/v2/accessToken'
));
Http::assertSent(fn ($request) => str_contains($request->url(), "{$oauthApi}/oauth/v2/accessToken"));
});