Testing
Every call the package makes goes through Laravel’s HTTP client. Http::fake() therefore replaces all of them, and no test has to reach LinkedIn. Http::preventStrayRequests() makes a test fail when a call slips past your fakes.
A complete example
This test covers the job from the quick start. It stores a connected account, fakes the answer of the Posts API, runs the job and checks what was sent.
// tests/Feature/PublishToLinkedInTest.php
namespace Tests\Feature;
use App\Jobs\PublishToLinkedIn;
use Darvis\ApiLinkedin\Models\LinkedInAccount;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class PublishToLinkedInTest extends TestCase
{
use RefreshDatabase;
public function test_it_publishes_the_text_on_the_member_profile(): void
{
Http::preventStrayRequests();
Http::fake([
'api.linkedin.com/rest/posts' => Http::response('', 201, [
'x-restli-id' => 'urn:li:share:123',
]),
]);
LinkedInAccount::create([
'member_id' => 'abc123',
'member_urn' => 'urn:li:person:abc123',
'name' => 'Test Member',
'access_token' => 'fake-access-token',
'scopes' => 'openid profile w_member_social',
'token_expires_at' => now()->addDays(30),
]);
(new PublishToLinkedIn('Hello LinkedIn'))->handle();
Http::assertSent(fn (Request $request) => $request->url() === 'https://api.linkedin.com/rest/posts'
&& $request['author'] === 'urn:li:person:abc123'
&& $request['commentary'] === 'Hello LinkedIn');
}
}
RefreshDatabase runs the package migration too, so the linkedin_accounts table exists. The account has a token that is valid for thirty days, so the package does not try to refresh it. The post id comes from the x-restli-id response header, not from the body: leave the header out of the fake and urn and permalink come back empty.
The package escapes reserved characters in the text. A text with a # arrives as \#, so assert on the escaped form.
The addresses to fake
| Call | Address | A successful answer |
|---|---|---|
| Code exchange and token refresh | www.linkedin.com/oauth/v2/accessToken | JSON with access_token, and optionally expires_in, refresh_token, refresh_token_expires_in, scope |
| Member profile during a connect | api.linkedin.com/v2/userinfo | JSON with sub and name |
| Publishing a post | api.linkedin.com/rest/posts | Status 201 with the header x-restli-id |
| Listing company pages | api.linkedin.com/rest/organizationAcls* | JSON with elements, each with an organization URN |
| Starting an image upload | api.linkedin.com/rest/images* | JSON with value.uploadUrl and value.image |
| Sending the image bytes | the uploadUrl from the previous answer | Any 2xx status |
Testing a failure
Fake an error status to get a LinkedInApiException, or throw Laravel’s ConnectionException to simulate an unreachable LinkedIn:
use Darvis\ApiLinkedin\Exceptions\LinkedInApiException;
use Darvis\ApiLinkedin\Facades\LinkedIn;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
// LinkedIn says no: $e->status is 422 and $e->body holds the JSON below
Http::fake([
'api.linkedin.com/rest/posts' => Http::response(['message' => 'Duplicate'], 422),
]);
// LinkedIn cannot be reached: $e->status is 0 and $e->isConnectionProblem() is true
Http::fake(fn () => throw new ConnectionException('Connection timed out'));
try {
LinkedIn::postAsMember('Hello');
} catch (LinkedInApiException $e) {
// assert on $e->operation, $e->status, $e->isConnectionProblem()
}
Use one of the two fakes per test. Without a stored account every facade method throws LinkedInNotConnected before any call, so a test of that case needs no fake at all.
Testing your own connect flow
The built-in routes need a signed-in user. In a feature test, sign in with actingAs() and put the state in the session:
use App\Models\User;
use Illuminate\Support\Facades\Http;
Http::fake([
'www.linkedin.com/oauth/v2/accessToken' => Http::response([
'access_token' => 'fake-access-token',
'expires_in' => 5184000,
'scope' => 'openid profile w_member_social',
]),
'api.linkedin.com/v2/userinfo' => Http::response(['sub' => 'abc123', 'name' => 'Test Member']),
]);
$this->actingAs(User::factory()->create())
->withSession(['linkedin_oauth_state' => 'known-state'])
->get(route('linkedin.callback', ['code' => 'any-code', 'state' => 'known-state']))
->assertSessionHas('linkedin_status', 'LinkedIn connected as Test Member.');
When your config adds an ability such as can:manage-linkedin, the user in the test has to pass that gate.