I'm using Socialite in a Laravel application to allow users to connect via Github.
My login controller contains the following two methods:
/**
* GET /login/github
* Redirect the user to the GitHub authentication page.
*/
public function redirectToProvider()
{
return Socialite::driver('github')->redirect();
}
/**
* GET /login/github/callback
* Obtain the user information from GitHub.
*/
public function handleProviderCallback(Request $request)
{
$githubUser = Socialite::driver('github')->user();
// Actual login procedures go here; redacted for brevity
return redirect('/');
}
When I manually test these methods in the browser, they work as expected. I visit /login/github where I'm redirected to Github to authenticate, then I'm sent back to /login/github/callback?state=somelongrandomkey which then redirects me home (/).
I'm also attempting to test these methods via Laravel Dusk, mocking Socialite.
My Dusk test method looks like this:
public function testReceivesGithubRequestAndCreatesNewUser()
{
$this->browse(function (Browser $browser) {
$user = factory('App\Models\User')->create([
'github_token' => 'foobar',
'github_username' => 'foobar'
]);
# Mock 1 - A Socialite user
$abstractUser = Mockery::mock('Laravel\Socialite\Two\User');
# Mock 2 - Socialite's Github provider
$provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');
$provider->shouldReceive('user')
->andReturn($abstractUser);
# Mock 3 - Socialite
Socialite::shouldReceive('driver')
->with('github')
->andReturn($provider);
$browser->visit('/login/github/callback')->assertPathIs('/');
});
When I run this test, the visit to /login/github/callback fails with an InvalidStateException.
From the log:
dusk.local.ERROR: {"exception":"[object] (Laravel\\Socialite\\Two\\InvalidStateException(code: 0): at /redacted/vendor/laravel/socialite/src/Two/AbstractProvider.php:210)
[stacktrace]
#0 /redacted/app/Http/Controllers/Auth/LoginController.php(84): Laravel\\Socialite\\Two\\AbstractProvider->user()
[...etc...]
When I trace where the error is coming from in AbstractProvider
I see it's attempting to compare state from the session with state from the query string:
protected function hasInvalidState()
{
if ($this->isStateless()) {
return false;
}
$state = $this->request->session()->pull('state');
return ! (strlen($state) > 0 && $this->request->input('state') === $state);
}
In my Dusk test, when /login/github/callback is visited, there is no state on the query string, so it's logical that it's failing.
I feel I'm missing some key component in setting up the mocks that provides that state, but I'm not sure what.
My test is built using these two examples for reference:
How to Test Laravel Socialite
How I write integration tests for Laravel Socialite powered apps
There's a fundamental difference between Dusk tests and the examples you're mentioning: Dusk opens the website in an actual browser and so the test and your application run in separate processes. That's why mocking doesn't work in Dusk tests.
The idea behind such an integration test is that you simulate a real user, without mocking or any other "shortcuts". In your case, that would mean logging in with a real GitHub account.
Related
I have 5 projects which are successfully deployed.... i need to give access to the user by single login to all these projects...
For example : I have 5 web app seprately which are deployed as
https://example.com/project1
https://example.com/project2
https://example.com/project3
https://example.com/project4
https://example.com/project5
and have their separate sql...
I need to login the user at very first and then the user can access all these web app and their working should go on with their respective sql....
these projects are created on laravel so, Right now the laravel auth is working and they have their own login system
All i need is to login user once and they can access all these apps
and user should login with 6th SQL(means another SQL)
In same situation, i made one more project only for authentification with Laravel Passport and add this project as socialite provider to other applications.
With this flow you have one entry point and user management for all projects.
First - make auth project. You need Laravel and Laravel Passport. See docs here. Optional - make user management ui (adding, removing, etc). Goal - users can login with their login/password.
Second - add ability to login for other projects with laravel/socialite.
Install laravel/socialite and socialiteproviders/manager
Create your own provider for your auth project - to example see the docs in socialiteproviders/manager
Implement LoginController in your apps for socialite. Some example:
class LoginController extends Controller
{
// Custom service for work with socialite - create or find users, etc
private SocialiteService $service;
public function __construct(SocialiteService $service)
{
$this->middleware('guest')->except(['logout', 'callback']);
$this->service = $service;
}
public function showLoginForm()
{
return view('pages.login');
}
public function login()
{
return $this->service->redirectToAuth();
}
public function callback(Request $request)
{
try{
$user = $this->service->getUser($request);
auth()->login($user);
return redirect()->route('index');
}catch (SocialiteException $exception){
return redirect()->route('login')->withErrors([
'error' => $exception->getMessage(),
'error_description' => $exception->getErrorDescription()
]);
}
}
public function logout(Request $request)
{
auth()->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login');
}
}
And after that you have SSO for all your apps with dedicated auth service.
Hint: create a private package with provider, service and all you auth logic. With this you can simply add this auth flow to feature projects and avoid duplicated code.
I am working on a Laravel project that is accessing an API for all backend data storage. This includes the user authentication.
I have an API Service as well as a API Service Provider that substantiates the Service and then feeds that service to all of the pages with the request var.
I figured it was easier just to substantiate the API Service in the Test Class so that's what I am doing but I am running into issues with the sessions.
Here is what I have so far:
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Services\ApiService;
class LoginTest extends TestCase
{
private $apiService;
public function __construct(){
$this->withSession(['user' => '', 'auth_token' => '']);
$url = env('API_URL');
$this->$apiService = new ApiService($url);
}
I am getting this error:
Call to a member function isStarted() on null.
Is there a better way to call an API Service that relies on session variables for testing?
How we ended up doing this:
We changed my constructor to a parent::SetUp to call the API Service
public function setUp(): void
{
parent::setUp();
$url = env('API_URL');
$this->ApiService = new ApiService($url);
}
Then in tests/TestCase we called post login routing passing the hard coded test username and password info.
public function login()
{
$this->post("/login", ['email' => 'admin#big-api.test', 'password' => 'ThisUserWillBeDeleted']);
}
Now I just call the login function on each of my tests cases where I need to be logged in through the API in order to access a page.
This ended up being a lot simpler than I thought but it took a bit to get there. Hope this is helpful for someone else.
In this project we use Algolia Search installed through Composer. When I'm running a dusk test on the login form it fails because of an error. The login on the form itself performs wel, it seems when this test actually logs in and ends up on the homescreen, this is where it fails.
Note: There is an Algolia APP_ID and SECRET defined in the .env file, and all is working fine when using the application.
The actual error output for this test:
1) Tests\Browser\LoginTest::testLogin
Algolia\AlgoliaSearch\Exceptions\UnreachableException: Impossible to connect, please check your Algolia Application Id.
Dusk test:
public function testLogin()
{
$user = factory(User::class)->create([
'email' => 'dusktester#mail.com',
'password' => '***'
]);
$this->browse(function (Browser $browser) use ($user) {
$browser->visit('/login')
->type('email', 'dusktester#mail.com')
->type('password', '***!')
->press('.button')
->assertPathIs('/');
});
}
Solved it another way. Our login system is a little more complex and links another table depending on the type of user you are. Since this wasn't defined in my user factory within this Dusk test it lacked some crucial information about this user which led to the Algolia Search error.
The way I solved it:
No longer creating a user within the Dusk test and use one of my already seeded test users. The credentials for this user are taken from my .env file to ensure a clean / safe dusk test file that can be uploaded to Git:
public function testLogin()
{
$this->browse(function (Browser $browser){
$browser->visit('/login')
->type('email', env('DUSK_USER'))
->type('password', env('DUSK_PASSWORD'))
->press('.button')
->assertPathIs('/');
});
}
I encounter a small problem when performing unit tests for the default Passport 5.8 routes.
In fact I tested the route / oauth / clients in get mode:
/** #test */
public function getOauthClients()
{
$user = factory(User::class)->make();
$response = $this->actingAs($user)->getJson('/oauth/clients');
$response->assertSuccessful();
}
But when I want to test the route provided by default in get mode: /oauth/token , I do not know what are the steps I need to follow.
Thank you in advance.
You should try with:
Passport::actingAs(
factory(User::class)->create()
);
$response = $this->getJson('/oauth/clients');
// ...
Passport ship with some testing helpers for that purpose, like the actingAs method above.
Quoting from documentation:
Passport's actingAs method may be used to specify the currently authenticated user as well as its scopes. The first argument given to the actingAs method is the user instance and the second is an array of scopes that should be granted to the user's token:
Currently I'm trying to write feature tests for laravel nova that assert that the page is loaded correctly and data can be seen.
However when I write the tests I can't find a way to assert that the correct text is shown due to way laravel nova's data is produce. Ontop of that I can't seem to test if a page loads correctly with laravel nova's 404 page coming back as a 200 response when a resource page that doesn't exist loads.
Has anyone found a good way to feature test nova?
TL;DR: check out this repo: https://github.com/bradenkeith/testing-nova. It has helped me find my way on how to test Laravel Nova.
Laravel Nova is basically a CRUD framework. So I'm assuming that, when you say
"that the page is loaded correctly and data can be seen"
You actually mean: my resources are loaded correctly. Or, I can create/update/delete a resource. This is because Nova is loading its resource info asynchronous via api calls.
So that's why, a good method to test your logic is to test the /nova-api/ routes.
For example:
<?php
namespace Tests\Feature\Nova;
use App\Note;
use Tests\TestCase;
class NoteTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
}
/** #test */
public function it_gets_a_note()
{
// given
$note = factory(Note::class)->create();
$response = $this->get('/nova-api/notes/' . $note->id)
->assertStatus(200)
->assertJson([
'resource' => [
'id' => [
'value' => $note->id
]
]
]);
}
}
By calling the route https://my-app.test/resources/notes/1, we can assert that we're getting a 200 response (successful) and that we're actually returning our newly created Note. This is a pretty trustworthy test to be sure a resource detail page is working fine.
If, however, you are talking about Browser Testing, you might want to take a look at Laravel Dusk:
Laravel Dusk provides an expressive, easy-to-use browser automation and testing API.
Once installed, you can start real browser testing:
$user = factory(User::class)->create();
$user->assignRole('admin');
$this->browse(function (Browser $browser) use ($user) {
$browser
->loginAs($user)
->visit('/')
->assertSee('Dashboard');
});
I had the same issue, I found out that the gate in App\Providers\NovaServiceProvider.php is not letting users pass, just return true when testing only and everything must work as expected
protected function gate()
{
Gate::define('viewNova', function ($user) {
return true;
});
}
Add on app/config file in your project directory:
App\Providers\NovaServiceProvider::class,