Not able to test the Laravel socialite - php

I'm trying to perform unit test for Laravel socialite function. There are 2 functions for redirect and callback.
There is one problem that I couldn't test successful was the callback function.
Here is how I test the redirect function
AuthController.php
/**
* #return mixed
*/
public function loginWithGoogle () {
return Socialite::driver('google')
->scopes([
'https://www.googleapis.com/auth/userinfo.email'
])
->with([
'access_type' => 'offline',
'prompt' => 'consent select_account'
])
->redirect();
}
AuthTest.php
public function testGoogleLoginForwardRequest()
{
$response = $this->getJson('/oauths/forward/google');
$response->assertStatus(302);
}
For the callback function
AuthController.php
/**
* #return RedirectResponse
*/
public function loginRedirectFromGoogle (): RedirectResponse
{
$user = Socialite::driver('google')->stateless()->user();
$acUser = User::where('email', $user->getEmail())->first();
if(empty($acUser)){
return redirect('/oauth/login?'.http_build_query([
'force_external_oauth_response' => 'true',
'external_oauth_response_error' => 'User account not register'
]));
}
Auth::login($acUser);
OAuthProvider::updateOrCreate(
[
'provider_name' => 'google',
'provider_id' => $user->id,
'user_id' => $acUser->id
],
[
'encrypted_token' => Crypt::encryptString($user->token),
'encrypted_refreshed_token' => Crypt::encryptString($user->refreshToken),
'expires_at' => time() + $user->expiresIn // time and expiresIn are all in seconds
]
);
return redirect('/oauth/login?'.http_build_query([
'force_external_oauth_response' => 'true',
'external_oauth_response_error' => ''
]));
}
AuthTest.php
public function testGoogleLoginRedirectRequest()
{
Mockery::getConfiguration()->allowMockingNonExistentMethods(false);
$abstractUser = Mockery::mock('Laravel\Socialite\Two\User');
$abstractUser
->shouldReceive('getId')
->andReturn(rand())
->shouldReceive('getName')
->andReturn($this->faker->name())
->shouldReceive('getEmail')
->andReturn($this->faker->name() . '#gmail.com')
->shouldReceive('getAvatar')
->andReturn('https://en.gravatar.com/userimage');
Socialite::shouldReceive('driver')->with('google')->andReturn($abstractUser);
$response = $this->getJson('/oauths/forward/google');
$response->assertStatus(302);
}
For this function it returns error the following
Mockery\Exception\BadMethodCallException: Method Mockery_0_Laravel_Socialite_Two_User::scopes() does not exist on this mock object in C:\xampp\htdocs\ac-dev\achievement-console\vendor\mockery\mockery\library\Mockery\Loader\EvalLoader.php(34) : eval()'d code:928
For the callback function, I followed this example How to Test Laravel Socialite

Related

Laravel test class property not persisting

I'm trying to set a sensorId property that will be used by every method in my feature test. The problem is that it is not persisting between tests.
Here is my test class:
class LogApiPostTest extends TestCase
{
public $sensorId;
public function setUp(): void
{
parent::setUp();
$this->sensorId = 'SEN' . rand(10000000, 999999999);
}
public function test_creates_sensor()
{
$response = $this->post('/api/logs', $this->data([
//
]));
$response->assertStatus(200);
$response->assertSeeText('Sensor created');
}
public function test_updates_sensor()
{
$response = $this->post('/api/logs', $this->data([
//
]));
$response->assertStatus(200);
$response->assertSeeText('Sensor updated');
}
public function test_creates_car()
{
$response = $this->post('/api/logs', $this->data([
'CarID' => 'B',
]));
$response->assertStatus(200);
$response->assertSeeText('Car created');
}
public function test_updates_car()
{
$response = $this->post('/api/logs', $this->data([
'CarID' => 'B',
]));
$response->assertStatus(200);
$response->assertSeeText('Car updated');
}
public function test_is_unauthorized()
{
$response = $this->post('/api/logs', $this->data([
'token_id' => 'thisShouldntWork',
]));
$response->assertUnauthorized();
}
public function data($merge = [])
{
return array_merge([
'token_id' => config('api.token'),
'sensorID' => $this->sensorId,
'CarID' => 'A',
'LogType' => 'CarLog',
'Time' => time(),
'DrctnFlr' => '-02',
'MCSS' => 'STB',
'OpMode' => 'INA',
'DoorStats' => '][**',
'DoorCmd' => 'C6:C1>',
'OCSS' => 'GTN02',
'Load' => '002%',
], $merge);
}
}
I just want the sensorId property to persist for all tests in this class.
When there is a dependency between tests, you must use the #depends annotation.
This allows to ensure the execution order of tests is correct and also to pass values between tests. Instead of making sensorId to persist, just pass it from test to test.
https://phpunit.readthedocs.io/en/9.5/writing-tests-for-phpunit.html#test-dependencies

How to test Laravel Socialite with Google Login

I am building an application that has features to login with google.
Here is the function to login with google with redirect
/**
* #return mixed
*/
public function loginWithGoogle () {
return Socialite::driver('google')
->scopes([
'https://www.googleapis.com/auth/userinfo.email'
])
->with([
'access_type' => 'offline',
'prompt' => 'consent select_account'
])
->redirect();
}
Below is the function to redirect from google login
/**
* #return RedirectResponse
*/
public function loginRedirectFromGoogle (): RedirectResponse
{
$user = Socialite::driver('google')->stateless()->user();
$acUser = User::where('email', $user->getEmail())->first();
if(empty($acUser)){
return redirect('/oauth/login?'.http_build_query([
'force_external_oauth_response' => 'true',
'external_oauth_response_error' => 'User account not register'
]));
}
Auth::login($acUser);
OAuthProvider::updateOrCreate(
[
'provider_name' => 'google',
'provider_id' => $user->id,
'user_id' => $acUser->id
],
[
'encrypted_token' => Crypt::encryptString($user->token),
'encrypted_refreshed_token' => Crypt::encryptString($user->refreshToken),
'expires_at' => time() + $user->expiresIn // time and expiresIn are all in seconds
]
);
return redirect('/oauth/login?'.http_build_query([
'force_external_oauth_response' => 'true',
'external_oauth_response_error' => ''
]));
}
Now I am trying to write the Unit Test for these two functions. I could not figure it out the way to do it.
Here are routes
Route::get('/oauths/redirect', [AuthController::class, 'loginWithGoogle']);
Route::get('/oauths/callback', [AuthController::class, 'loginRedirectFromGoogle']);
Here is AuthControllerTest file
public function testRedirectWithGoogle()
{
$abstractUser = Mockery::mock('Laravel\Socialite\Two\User');
$abstractUser
->shouldReceive('getId')
->andReturn(rand())
->shouldReceive('getName')
->andReturn('test user')
->shouldReceive('getEmail')
->andReturn('test.user' . '#gmail.com')
->shouldReceive('getAvatar')
->andReturn('https://en.gravatar.com/userimage');
$provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');
$provider->shouldReceive('user')->andReturn($abstractUser);
Socialite::shouldReceive('driver')->with('google')
->andReturn($provider);
}

how to logout from rest api in yii2

I m calling logout api in yii2 framework using logout action
url :http://localhost/mobile/public/api/v1/logout
Code:
public function actionLogout()
{
$user_t = Yii::$app->user->logout();
return $this->apiItem(array(),'Logout Successfully');
}
but after calling logout api
when after this i calling view profile api it returns user data
public function actionViewprofile()
{
$user = Yii::$app->user->identity;
$profile_fetch = [
'firstname' => $user['member_fname'],
'lastname' => $user['member_lname'],
'gender' => $user['member_gender'],
'dateofbirth' => $user['member_dob']
];
return $this->apiItem($profile_fetch);
}
where apitem is a function for json parameter format
/**
* Api Item response
*/
public function apiItem($data, $message = false,$flag = false )
{
Yii::$app->response->statusCode = 200;
return [
'statusCode' => 200,
'message' => $message ? $message : 'Data retrieval successful',
'data' => $data,
'flag' => $flag
];
}
Clear the token from DB and clear the user session
$userID = Yii::$app->session->get('userID');
$userModel = User::find()->where(['id'=>$userID])->one();
if(!empty($userModel))
{
$userModel->token=NULL;
$userModel->save(false);
}
Yii::app()->user->logout(false);

Socialite: Merge accounts that have the same email

im using socialite to give the users the option to log in with either facebook or github. but when a user logs in with facebook and after that with github, 2 separate accounts are created. So my question is , is there a way to combine these 2 accounts into one? for example if a user that has logged in with facebook is using the same email address to log in with github, no new account will be created and they will simply be logged in
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('profile');
$table->string('slug');
$table->string('provider_id');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}
login/register code
/**
* Redirect the user to the provider authentication page.
*
* #return Response
*/
public function redirectToProvider($provider)
{
return Socialite::driver($provider)->redirect();
}
/**
* Obtain the user information from the provider.
*
* #return Response
*/
public function handleProviderCallback($provider)
{
$SocialUser = Socialite::driver($provider)->stateless()->user();
$user = $this -> findOrCreateUser($SocialUser,$provider);
auth()->login($user,true);
return redirect('/');
}
protected function findOrCreateUser($SocialUser,$provider)
{
$user = User::firstOrNew(['provider_id' => $SocialUser->id]);
if ($user->exists) return $user;
$user->fill([
'name' => $SocialUser->nickname?:$SocialUser->name,
'slug' => str_slug($SocialUser->nickname?:$SocialUser->name).'-'.uniqid(),
'email' => $SocialUser->email,
'avatar' => $SocialUser->avatar,
'profile' => Hash::make('no pic'),
'password' => Hash::make('no need for password token based'),
// 'website' => 'add a website',
// 'github_profile' => 'add github profile',
'email_notifications' => 1
])->save();
$user->assignRole('user');
\Mail::to($user)->send(new Welcome($user));
session()->flash('message','Welcome to '.config('app.name').' '.$user->name);
return $user;
}
}
I have a solution that's using the spirit of everything in this question and answer.
/**
* #param string $provider
* #param \Laravel\Socialite\Contracts\User $sUser
* #return \App\User|false
*/
protected function findOrCreateUser($provider, $sUser)
{
$oauthProvider = OAuthProvider::where('provider', $provider)
->where('provider_user_id', $sUser->id)
->first();
if ($oauthProvider) {
$oauthProvider->update([
'access_token' => $sUser->token,
'refresh_token' => $sUser->refreshToken ?? null,
]);
return $oauthProvider->user;
}
$user = User::firstWhere('email', $sUser->email);
if ($user) {
return $this->createUser($provider, $sUser, $user);
}
return $this->createUser($provider, $sUser);
}
/**
* If a User already exists for the email, skip user creation
* and add this provider to the list of `$user->oauthProviders`.
* #param string $provider
* #param \Laravel\Socialite\Contracts\User $sUser
* #param \App\User $user
* #return \App\User
*/
protected function createUser($provider, $sUser, User $user = null)
{
if (!$user) {
$user = User::create([
'name' => $sUser->name,
'email' => $sUser->email,
'email_verified_at' => now(),
]);
} else if ($user->email_verified_at === null) {
$user->email_verified_at = now();
$user->save();
}
$user->oauthProviders()->create([
'provider' => $provider,
'provider_user_id' => $sUser->id,
'access_token' => $sUser->token,
'refresh_token' => $sUser->refreshToken ?? null,
]);
return $user;
}
Before, it had a check for if User::where('email', $sUser->email), and if so, reject the request with an "email already taken" message.
With the oauth_providers table and $user->oauthProviders relationship (User hasMany OAuthProviders), rather than create a new User in the users table every time someone uses oauth, it attaches that oauth record with the existing user $user = User::firstWhere('email', $sUser->email);
If anyone wants a little more, I modified this repo here to make both GitHub and Twitter oauth work: https://github.com/cretueusebiu/laravel-vue-spa. Base yourself around OAuthController.
With the above code, I can register a user via the registration form to capture an email, then login as GitHub and Twitter and have my user plus two oauth providers.
Most of the magic of my solution comes in with the 3rd param on createUser. It will remain to be seen if it works better to leave createUser as always creating, and then make a new method called addProviderToUser. That might be slightly more code, but it might also be simpler and more friendly to unit tests.
Here are my oauth redirect and callback methods too, for science reasons:
/**
* Redirect the user to the provider authentication page. Twitter uses OAuth1.0a, and does not support
* Socialite::driver($provider)->stateless(), so library `abraham/twitteroauth` is used to handle everything.
*
* #param string $provider
* #return \Illuminate\Http\RedirectResponse
*/
public function redirectToProvider($provider)
{
if ($provider === 'twitter') {
$tempId = Str::random(40);
$connection = new TwitterOAuth(config('services.twitter.client_id'), config('services.twitter.client_secret'));
$requestToken = $connection->oauth('oauth/request_token', array('oauth_callback' => config('services.twitter.callback_url').'?user='.$tempId));
\Cache::put($tempId, $requestToken['oauth_token_secret'], 86400); // 86400 seconds = 1 day
$url = $connection->url('oauth/authorize', array('oauth_token' => $requestToken['oauth_token']));
} else {
$url = Socialite::driver($provider)->stateless()->redirect()->getTargetUrl();
}
return [
'url' => $url,
];
}
/**
* Obtain the user information from the provider.
*
* #param string $driver
* #return \Illuminate\Http\Response
*/
public function handleProviderCallback(Request $request, $provider)
{
if ($provider === 'twitter') {
$connection = new TwitterOAuth(config('services.twitter.client_id'), config('services.twitter.client_secret'), $request->oauth_token, \Cache::get($request->user));
$access_token = $connection->oauth('oauth/access_token', ['oauth_verifier' => $request->oauth_verifier]);
$connection = new TwitterOAuth(config('services.twitter.client_id'), config('services.twitter.client_secret'), $access_token['oauth_token'], $access_token['oauth_token_secret']);
$user = $connection->get('account/verify_credentials', ['include_email' => 'true']);
$user->token = $access_token['oauth_token'];
} else {
$user = Socialite::driver($provider)->stateless()->user();
}
$user = $this->findOrCreateUser($provider, $user);
$this->guard()->setToken(
$token = $this->guard()->login($user)
);
return view('oauth/callback', [
'token' => $token,
'token_type' => 'bearer',
'expires_in' => $this->guard()->getPayload()->get('exp') - time(),
]);
}
config/services.php
'github' => [
'client_id' => env('GITHUB_CLIENT_ID'),
'client_secret' => env('GITHUB_CLIENT_SECRET'),
'callback_url' => env('GITHUB_CALLBACK_URL'),
'provider_name' => env('GITHUB_PROVIDER_NAME', 'GitHub'),
],
'twitter' => [
'client_id' => env('TWITTER_CLIENT_ID'),
'client_secret' => env('TWITTER_CLIENT_SECRET'),
'callback_url' => env('TWITTER_CALLBACK_URL'),
'provider_name' => env('TWITTER_PROVIDER_NAME', 'Twitter'),
],
.env
# localhost
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_CALLBACK_URL=https://valet.test/api/oauth/github
TWITTER_CLIENT_ID=
TWITTER_CLIENT_SECRET=
TWITTER_CALLBACK_URL=https://valet.test/api/oauth/twitter/callback
You'd have to look in the above sample repo to figure out how those env variables are being consumed, but hint: look at spa.blade.php, vuex, and api.php
try changing your code to this:
$user = User::where('email', $SocialUser->email)->first();
if (!empty($user) && in_array($SocialUser->id, $user->provider_id) ) {
session()->flash('message','Welcome to '.config('app.name').' '.$user->name);
return $user;
}
if (empty($user) ) {
$user = User::create([
'name' => $SocialUser->nickname?:$SocialUser->name,
'slug' => str_slug($SocialUser->nickname?:$SocialUser->name).'-'.uniqid(),
'email' => $SocialUser->email,
'avatar' => $SocialUser->avatar,
'profile' => Hash::make('no pic'),
'password' => Hash::make('no need for password token based'),
// 'website' => 'add a website',
// 'github_profile' => 'add github profile',
'email_notifications' => 1,
'provider_id' => [$SocialUser->id]
]);
$user->assignRole('user');
\Mail::to($user)->send(new Welcome($user));
session()->flash('message','Welcome to '.config('app.name').' '.$user->name);
return $user;
}
$providers = array_push($user->provider_id, $SocialUser->id);
$user->update([
'provider_id' => $providers
]);
session()->flash('message','Welcome to '.config('app.name').' '.$user->name);
return $user;
You are best also adding this to your User model:
protected $casts = [
'provider_id' => 'array'
];
I hope this helps

Laravel 5.7 request validation doesn't work as expected

I use Laravel to submit a form. Here is my web.php routes :
Route::middleware(['auth'])->prefix('account')->namespace('Account')->group(function () {
Route::get('informations', 'AccountController#index')->name('account.informations');
Route::post('informations', 'AccountController#update')->name('account.informations.post');
});
My Controller AccountController.php :
/**
* #param UpdateMember $request
* #return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateUser $request)
{
dd($request->all());
$user = User::where('id', Auth::user()->id)
->update($request->all());
return redirect()->route('account.informations');
}
And my UpdateUser.php :
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'lastname' => 'required|string|max:255',
'firstname' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users|confirmed',
'password' => 'required|string|min:6|confirmed',
];
}
My problem : when I use UserUpdate $request in my controller, I don't reach the function, the dd($request->all()) is not shown.
But if I replace :
public function update(UpdateUser $request)
By
public function update(Request $request)
My controller is reached. What do I do wrong ?
what about the unique field validation like email because your validation rules returns a error email already exists while updating a existing record. This can be solved as example below
public function rules()
{
$client = Client::find($this->client);
return [
'name' => 'required|string',
'address' => 'required|string',
'gender' => 'required|string',
'dob' => 'required|string',
'phone' => 'required|string',
'email' => 'required|unique:clients,email,' . $client->id,
'password' => 'regex:/^[0-9a-zA-Z]*$/',
'profile-photo' => 'image|mimes:jpg,png,jpeg',
'c_name' => 'nullable|regex:/^[a-zA-Z ]*$/',
'c_address' => 'nullable|regex:/^[a-zA-Z ]*$/',
'c_email' => 'nullable|email|unique:clients,company_email,'. $client->id,
'c_contact' => 'nullable|regex:/^[0-9]*$/',
];
}
In my case, the reason was that I forgt to add "Accept: application/json" in the request header. After adding that header everything worked fine

Categories