From 04975020e417802f35fa8f80afef7be5b0af1ea4 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Tue, 19 May 2026 09:37:16 -0300 Subject: [PATCH] test(social): tests pull OAuth URLs from config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CLAUDE.md | 7 +++++++ .../Social/ConnectionVerifierTest.php | 20 +++++++++++-------- .../Social/LinkedInPageAnalyticsTest.php | 12 +++++------ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 651d0c6f..f8e47ba8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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.` 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 diff --git a/tests/Feature/Services/Social/ConnectionVerifierTest.php b/tests/Feature/Services/Social/ConnectionVerifierTest.php index 34c94f92..4b16294d 100644 --- a/tests/Feature/Services/Social/ConnectionVerifierTest.php +++ b/tests/Feature/Services/Social/ConnectionVerifierTest.php @@ -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'), ], diff --git a/tests/Feature/Services/Social/LinkedInPageAnalyticsTest.php b/tests/Feature/Services/Social/LinkedInPageAnalyticsTest.php index 0836940d..6805fd4d 100644 --- a/tests/Feature/Services/Social/LinkedInPageAnalyticsTest.php +++ b/tests/Feature/Services/Social/LinkedInPageAnalyticsTest.php @@ -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")); });