Skip to content
23 changes: 23 additions & 0 deletions src/Socialite.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Facades\Facade;
use Laravel\Socialite\Contracts\Factory;
use Laravel\Socialite\Testing\SocialiteFake;

/**
* @method static \Laravel\Socialite\Contracts\Provider driver(string $driver = null)
Expand All @@ -27,4 +28,26 @@ protected static function getFacadeAccessor()
{
return Factory::class;
}

/**
* Register a fake Socialite instance.
*
* @param string $driver
* @param \Laravel\Socialite\Contracts\User|\Closure|array|null $user
* @return \Laravel\Socialite\Testing\SocialiteFake
*/
public static function fake(string $driver, $user = null)
{
$root = static::getFacadeRoot();

if ($root instanceof SocialiteFake) {
$fake = $root;
} else {
$fake = new SocialiteFake($root);

static::swap($fake);
}

return $fake->fake($driver, $user);
}
}
103 changes: 103 additions & 0 deletions src/Testing/FakeProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

namespace Laravel\Socialite\Testing;

use Closure;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Traits\ForwardsCalls;
use Laravel\Socialite\Contracts\Provider;

class FakeProvider implements Provider
{
use ForwardsCalls;

/**
* The driver name.
*
* @var string
*/
protected $driver;

/**
* The provider resolver.
*
* @var \Closure
*/
protected $resolver;

/**
* The original provider instance.
*
* @var \Laravel\Socialite\Contracts\Provider
*/
protected $provider;

/**
* The fake user to return.
*
* @var \Laravel\Socialite\Contracts\User|\Closure|array|null
*/
protected $user = null;

/**
* Create a new fake provider instance.
*
* @param string $driver
* @param \Closure $resolver
* @param \Laravel\Socialite\Contracts\User|\Closure|array|null $user
*/
public function __construct($driver, $resolver, $user = null)
{
$this->driver = $driver;
$this->resolver = $resolver;
$this->user = $user;
}

/**
* Redirect the user to the authentication page for the provider.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Illuminate\Http\RedirectResponse
*/
public function redirect()
{
return new RedirectResponse('https://socialite.fake/'.$this->driver.'/authorize');
}

/**
* Get the User instance for the authenticated user.
*
* @return \Laravel\Socialite\Contracts\User
*/
public function user()
{
if ($this->user instanceof Closure) {
return ($this->user)();
}

return $this->user ?? $this->provider()->user();
}

/**
* Get the original provider instance.
*
* @return \Laravel\Socialite\Contracts\Provider
*/
public function provider()
{
if (isset($this->provider)) {
return $this->provider;
}

return $this->provider = ($this->resolver)();
}

/**
* Handle calls to methods that are not available on the fake provider.
*
* @param string $method
*/
public function __call($method, array $parameters)
{
return $this->forwardCallTo($this->provider(), $method, $parameters);
}
}
61 changes: 61 additions & 0 deletions src/Testing/SocialiteFake.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace Laravel\Socialite\Testing;

use Laravel\Socialite\Contracts\Factory;

class SocialiteFake implements Factory
{
/**
* The original factory instance.
*
* @var \Laravel\Socialite\Contracts\Factory
*/
protected $factory;

/**
* The fake provider instances.
*
* @var array<string, \Laravel\Socialite\Testing\FakeProvider>
*/
protected $providers = [];

/**
* Create a new Socialite fake instance.
*
* @param \Laravel\Socialite\Contracts\Factory $factory
*/
public function __construct($factory)
{
$this->factory = $factory;
}

/**
* Get an OAuth provider implementation.
*
* @param string $driver
* @return \Laravel\Socialite\Contracts\Provider
*/
public function driver($driver = null)
{
return $this->providers[$driver] ?? $this->factory->driver($driver);
}

/**
* Register a fake user for the given driver.
*
* @param string $driver
* @param \Laravel\Socialite\Contracts\User|\Closure|array|null $user
* @return $this
*/
public function fake($driver, $user = null)
{
$resolver = function () use ($driver) {
return $this->factory->driver($driver);
};

$this->providers[$driver] = new FakeProvider($driver, $resolver, $user);

return $this;
}
}
131 changes: 131 additions & 0 deletions tests/SocialiteFakeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

namespace Laravel\Socialite\Tests;

use Laravel\Socialite\Contracts\Factory;
use Laravel\Socialite\Socialite;
use Laravel\Socialite\SocialiteServiceProvider;
use Laravel\Socialite\Testing\FakeProvider;
use Laravel\Socialite\Testing\SocialiteFake;
use Laravel\Socialite\Two\GoogleProvider;
use Laravel\Socialite\Two\User as OAuth2User;
use Orchestra\Testbench\TestCase;

class SocialiteFakeTest extends TestCase
{
protected function getPackageProviders($app)
{
return [SocialiteServiceProvider::class];
}

protected function tearDown(): void
{
Socialite::clearResolvedInstances();

parent::tearDown();
}

public function test_it_can_fake_a_driver_with_a_user()
{
$user = (new OAuth2User)->map([
'id' => '123',
'name' => 'Test User',
'email' => '[email protected]',
]);

Socialite::fake('github', $user);

$this->assertInstanceOf(SocialiteFake::class, $this->app->make(Factory::class));
$this->assertInstanceOf(FakeProvider::class, Socialite::driver('github'));

$retrievedUser = Socialite::driver('github')->user();

$this->assertSame('123', $retrievedUser->getId());
$this->assertSame('Test User', $retrievedUser->getName());
$this->assertSame('[email protected]', $retrievedUser->getEmail());
}

public function test_it_can_fake_a_driver_with_a_closure()
{
Socialite::fake('github', function () {
return (new OAuth2User)->map([
'id' => '456',
'name' => 'Closure User',
'email' => '[email protected]',
]);
});

$user = Socialite::driver('github')->user();

$this->assertSame('456', $user->getId());
$this->assertSame('Closure User', $user->getName());
}

public function test_it_can_fake_multiple_drivers()
{
Socialite::fake('github', (new OAuth2User)->map(['id' => 'github-123']));
Socialite::fake('google', (new OAuth2User)->map(['id' => 'google-456']));

$this->assertSame('github-123', Socialite::driver('github')->user()->getId());
$this->assertSame('google-456', Socialite::driver('google')->user()->getId());
}

public function test_it_returns_fake_redirect_response()
{
Socialite::fake('github', (new OAuth2User)->map(['id' => '123']));

$response = Socialite::driver('github')->redirect();

$this->assertSame('https://socialite.fake/github/authorize', $response->getTargetUrl());
}

public function test_it_forwards_calls_to_the_real_provider_methods()
{
$this->app['config']->set('services.github', [
'client_id' => 'test-client-id',
'client_secret' => 'test-client-secret',
'redirect' => 'http://localhost/callback',
]);

Socialite::fake('github', (new OAuth2User)->map(['id' => '123']));

$provider = Socialite::driver('github');

// Verify that methods are forwarded to the real provider
$provider->stateless();
$provider->scopes(['user', 'repo']);
$provider->setScopes(['user:email']);
$provider->redirectUrl('http://example.com/callback');
$provider->with(['custom' => 'param']);
$provider->enablePKCE();

// Verify that the fake user is returned despite calling other methods
$user = $provider->user();

$this->assertSame('123', $user->getId());
}

public function test_it_returns_real_driver_when_not_faked()
{
$this->app['config']->set('services.github', [
'client_id' => 'test-client-id',
'client_secret' => 'test-client-secret',
'redirect' => 'http://localhost/callback',
]);

$this->app['config']->set('services.google', [
'client_id' => 'test-client-id',
'client_secret' => 'test-client-secret',
'redirect' => 'http://localhost/callback',
]);

// Fake only github
Socialite::fake('github', (new OAuth2User)->map(['id' => '123']));

// Github should return the fake provider
$this->assertInstanceOf(FakeProvider::class, Socialite::driver('github'));

// Google should return the real provider since it wasn't faked
$this->assertInstanceOf(GoogleProvider::class, Socialite::driver('google'));
}
}