laracoft's avatar

API client testing

My actual controller code has something like this

$response = Http::acceptJson()
    ->withHeaders([
        'Accept-Language' => 'en_US',
        'Authorization' => 'Bearer '.$this->accessToken,
    ])

My test code has something like this

Http::expects('post')->with('https://example.com/v1/oauth2/token')
    ->andReturns($accessTokenResponse);
  1. I wrote the above based on Socialite's test https://github.com/laravel/socialite/blob/5.x/tests/LinkedInProviderTest.php
  2. What I really like is how short it is, however it uses Guzzle.
  3. What I understood from https://laravel.com/docs/10.x/http-client, is that the Http:: facade is actually Guzzle.
  4. But I get the exception Mockery\Exception\BadMethodCallException: Method Mockery_6_Illuminate_Http_Client_Factory::acceptJson() does not exist on this mock object
  5. It is also quite peculiar that none of Laravel's own repositories utilize Http:: in their tests, I checked socialite, cashier-stripe and slack-notification-channel

Any examples of using Http:: in tests? Or able to fix what I'm encountering?

0 likes
6 replies
tisuchi's avatar

@laracoft This could be the way to mock the http.

Http::fake([
    'https://example.com/v1/oauth2/token' => Http::response($accessTokenResponse)
]);
laracoft's avatar

I made a minimum example still causing the error

namespace App\Tests;

use App\Token;
use App\Tests\TestbenchTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Mockery as m;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;

/**
 * Test the API calls are formed correctly
 */
class TokenTest extends TestbenchTestCase
{
    use RefreshDatabase;
    use WithoutMiddleware;

    protected function tearDown(): void
    {
        parent::tearDown();

        m::close();
    }

    public function test_it_can_map_a_user_without_an_email_address()
    {
        $request = m::mock(Request::class);
        $request->allows('input')->with('code')->andReturns('fake-code');

        $stream = m::mock(StreamInterface::class);
        $stream->allows('__toString')->andReturns(json_encode(['access_token' => 'fake-token']));

        $accessTokenResponse = m::mock(ResponseInterface::class);

        $accessTokenResponse->allows('getBody')->andReturns($stream);

        $basicProfileStream = m::mock(StreamInterface::class);
        $basicProfileStream->allows('__toString')->andReturns(json_encode(['id' => $userId = 1]));

        $basicProfileResponse = m::mock(ResponseInterface::class);
        $basicProfileResponse->allows('getBody')->andReturns($basicProfileStream);

        $emailAddressStream = m::mock(StreamInterface::class);
        $emailAddressStream->allows('__toString')->andReturns(json_encode(['elements' => []]));

        $emailAddressResponse = m::mock(ResponseInterface::class);
        $emailAddressResponse->allows('getBody')->andReturns($emailAddressStream);

        Http::fake([
            'https://example.com/oauth2/token' => Http::response($accessTokenResponse),
        ]);

        $provider = new Token($request, 'client_id', 'client_secret', 'redirect');
        $referral_id = $provider
            ->getOauth2Token();

    }
}
namespace App\Token;

use Exception;
use GuzzleHttp\Client;
use Http;
use Illuminate\Http\Request;

class Token
{
    private $accessToken = null;

    private $trackingId = null;

    private $returnUrl = null;

    protected $version = 2;

    protected $body;

    protected Client $httpClient;

    /**
     * Create a new provider instance.
     *
     * @param  string  $clientId
     * @param  string  $clientSecret
     * @param  string  $redirectUrl
     * @param  array  $guzzle
     * @return void
     */
    public function __construct(Request $request, $clientId, $clientSecret, $redirectUrl, $guzzle = [])
    {
        $this->guzzle = $guzzle;
        $this->request = $request;
        $this->clientId = $clientId;
        $this->redirectUrl = $redirectUrl;
        $this->clientSecret = $clientSecret;
    }

    /**
     * Indicates that the provider should operate as stateless.
     *
     * @return $this
     */
    public function stateless()
    {
        $this->stateless = true;

        return $this;
    }

    /**
     * Set the Guzzle HTTP client instance.
     *
     * @return $this
     */
    public function setHttpClient(Client $client)
    {
        $this->httpClient = $client;

        return $this;
    }

    public function getOauth2Token()
    {
        $client = 'client';
        $secret = 'secret';

        $url = 'https://example.com/oauth2/token';
        $response = Http::acceptJson()
            ->withBasicAuth($client, $secret)
            ->asForm()
            ->withHeaders([
                'Accept-Language' => 'en_US',
            ])
            ->post($url, [
                'grant_type' => 'client_credentials',
            ]);

        $json = $response->json();

        if (! isset($json['access_token'])) {
            throw new Exception('Unable to get token');
        }

        return $json['access_token'];
    }
}
tisuchi's avatar

@laracoft

⚠️ Not tested, but I think this should be enough.

Happy Path

/** @test */
    public function it_can_get_oauth2_token()
    {
        // Mock the HTTP call
        Http::fake([
            'https://example.com/oauth2/token' => Http::response([
                'access_token' => 'mocked_access_token',
                'token_type' => 'Bearer',
                'expires_in' => 3600,
            ], 200)
        ]);

        // Instantiate your Token class
        $tokenProvider = new Token(request(), 'dummyClientId', 'dummyClientSecret', 'dummyRedirectUrl');

        // Call the method
        $token = $tokenProvider->getOauth2Token();

        // Assertions
        $this->assertEquals('mocked_access_token', $token);

        // Verify the correct request was made
        Http::assertSent(function ($request) {
            return $request->url() == 'https://example.com/oauth2/token' &&
                $request['grant_type'] == 'client_credentials' &&
                $request->header('Accept-Language') == 'en_US' &&
                $request->hasHeader('Authorization'); // for Basic Auth check
        });
    }

Unhappy Path

/** @test */
    public function it_throws_an_exception_when_token_is_missing()
    {
        // Mock the HTTP call with an invalid response
        Http::fake([
            'https://example.com/oauth2/token' => Http::response([
                'token_type' => 'Bearer',
                'expires_in' => 3600,
            ], 200)
        ]);

        // Instantiate your Token class
        $tokenProvider = new Token(request(), 'dummyClientId', 'dummyClientSecret', 'dummyRedirectUrl');

        // Expect an exception
        $this->expectException(Exception::class);
        $this->expectExceptionMessage('Unable to get token');

        // Call the method
        $tokenProvider->getOauth2Token();
    }
laracoft's avatar

@tisuchi

thanks, but that's throwing out the entire socialite example.

My aim is to utilize Http:: in my controller, but write my tests using the terse style of the socialite example.

Please or to participate in this conversation.